diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py index 3403c938a8..6cc48984ab 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main.py @@ -253,73 +253,3 @@ class TestAtomEditorComponentsMain(object): ) -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_generic']) -@pytest.mark.system -class TestMaterialEditorBasicTests(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project): - def delete_files(): - file_system.delete( - [ - os.path.join(workspace.paths.project(), "Materials", "test_material.material"), - os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"), - os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"), - ], - True, - True, - ) - # Cleanup our newly created materials - delete_files() - - def teardown(): - # Cleanup our newly created materials - delete_files() - - request.addfinalizer(teardown) - - @pytest.mark.parametrize("exe_file_name", ["MaterialEditor"]) - @pytest.mark.test_case_id("C34448113") # Creating a New Asset. - @pytest.mark.test_case_id("C34448114") # Opening an Existing Asset. - @pytest.mark.test_case_id("C34448115") # Closing Selected Material. - @pytest.mark.test_case_id("C34448116") # Closing All Materials. - @pytest.mark.test_case_id("C34448117") # Closing all but Selected Material. - @pytest.mark.test_case_id("C34448118") # Saving Material. - @pytest.mark.test_case_id("C34448119") # Saving as a New Material. - @pytest.mark.test_case_id("C34448120") # Saving as a Child Material. - @pytest.mark.test_case_id("C34448121") # Saving all Open Materials. - def test_MaterialEditorBasicTests( - self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name): - - expected_lines = [ - "Material opened: True", - "Test asset doesn't exist initially: True", - "New asset created: True", - "New Material opened: True", - "Material closed: True", - "All documents closed: True", - "Close All Except Selected worked as expected: True", - "Actual Document saved with changes: True", - "Document saved as copy is saved with changes: True", - "Document saved as child is saved with changes: True", - "Save All worked as expected: True", - ] - unexpected_lines = [ - # "Trace::Assert", - # "Trace::Error", - "Traceback (most recent call last):" - ] - - hydra.launch_and_validate_results( - request, - TEST_DIRECTORY, - generic_launcher, - "hydra_AtomMaterialEditor_BasicTests.py", - run_python="--runpython", - timeout=120, - expected_lines=expected_lines, - unexpected_lines=unexpected_lines, - halt_on_unexpected=True, - null_renderer=True, - log_file_name="MaterialEditor.log", - ) diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py index 58e5d00ff2..9bb7f9c50e 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py @@ -70,3 +70,75 @@ class TestAtomEditorComponentsSandbox(object): null_renderer=True, cfg_args=cfg_args, ) + +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +@pytest.mark.parametrize("launcher_platform", ['windows_generic']) +@pytest.mark.system +class TestMaterialEditorBasicTests(object): + @pytest.fixture(autouse=True) + def setup_teardown(self, request, workspace, project): + def delete_files(): + file_system.delete( + [ + os.path.join(workspace.paths.project(), "Materials", "test_material.material"), + os.path.join(workspace.paths.project(), "Materials", "test_material_1.material"), + os.path.join(workspace.paths.project(), "Materials", "test_material_2.material"), + ], + True, + True, + ) + # Cleanup our newly created materials + delete_files() + + def teardown(): + # Cleanup our newly created materials + delete_files() + + request.addfinalizer(teardown) + + @pytest.mark.parametrize("exe_file_name", ["MaterialEditor"]) + @pytest.mark.test_case_id("C34448113") # Creating a New Asset. + @pytest.mark.test_case_id("C34448114") # Opening an Existing Asset. + @pytest.mark.test_case_id("C34448115") # Closing Selected Material. + @pytest.mark.test_case_id("C34448116") # Closing All Materials. + @pytest.mark.test_case_id("C34448117") # Closing all but Selected Material. + @pytest.mark.test_case_id("C34448118") # Saving Material. + @pytest.mark.test_case_id("C34448119") # Saving as a New Material. + @pytest.mark.test_case_id("C34448120") # Saving as a Child Material. + @pytest.mark.test_case_id("C34448121") # Saving all Open Materials. + def test_MaterialEditorBasicTests( + self, request, workspace, project, launcher_platform, generic_launcher, exe_file_name): + + expected_lines = [ + "Material opened: True", + "Test asset doesn't exist initially: True", + "New asset created: True", + "New Material opened: True", + "Material closed: True", + "All documents closed: True", + "Close All Except Selected worked as expected: True", + "Actual Document saved with changes: True", + "Document saved as copy is saved with changes: True", + "Document saved as child is saved with changes: True", + "Save All worked as expected: True", + ] + unexpected_lines = [ + # "Trace::Assert", + # "Trace::Error", + "Traceback (most recent call last):" + ] + + hydra.launch_and_validate_results( + request, + TEST_DIRECTORY, + generic_launcher, + "hydra_AtomMaterialEditor_BasicTests.py", + run_python="--runpython", + timeout=120, + expected_lines=expected_lines, + unexpected_lines=unexpected_lines, + halt_on_unexpected=True, + null_renderer=True, + log_file_name="MaterialEditor.log", + ) + diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py index 2d8b124125..10a6ab1ef4 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections import Counter from collections import deque from os import path +from pathlib import Path from PySide2 import QtWidgets @@ -20,6 +21,10 @@ from editor_python_test_tools.utils import Report import azlmbr.entity as entity import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.editor as editor +import azlmbr.globals +import azlmbr.math as math import azlmbr.prefab as prefab import editor_python_test_tools.pyside_utils as pyside_utils @@ -57,26 +62,46 @@ class PrefabInstance: def __hash__(self): return hash(self.container_entity.id) - """ - See if this instance is valid to be used with other prefab operations. - :return: Whether the target instance is valid or not. - """ def is_valid(self) -> bool: + """ + See if this instance is valid to be used with other prefab operations. + :return: Whether the target instance is valid or not. + """ return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs - """ - Reparent this instance to target parent entity. - The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. - :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. - """ + def has_editor_prefab_component(self) -> bool: + """ + Check if the instance's container entity contains EditorPrefabComponent. + :return: Whether the container entity of target instance has EditorPrefabComponent in it or not. + """ + return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId) + + def is_at_position(self, expected_position): + """ + Check if the instance's container entity is at expected position given. + :return: Whether the container entity of target instance is at expected position or not. + """ + actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id) + is_at_position = actual_position.IsClose(expected_position) + + if not is_at_position: + Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}") + + return is_at_position + async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): + """ + Reparent this instance to target parent entity. + The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. + :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. + """ container_entity_id_before_reparent = self.container_entity.id original_parent = EditorEntity(self.container_entity.get_parent_id()) - original_parent_before_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} new_parent = EditorEntity(parent_entity_id) - new_parent_before_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) pyside_utils.run_soon(lambda: wait_for_propagation()) @@ -90,23 +115,28 @@ class PrefabInstance: except pyside_utils.EventLoopTimeoutException: pass - original_parent_after_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \ "The children count of the Prefab Instance's original parent should be decreased by 1." assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \ "This Prefab Instance is still a child entity of its original parent entity." - new_parent_after_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \ "The children count of the Prefab Instance's new parent should be increased by 1." - container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop() + after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys())) + container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()] reparented_container_entity = EditorEntity(container_entity_id_after_reparent) reparented_container_entity_parent_id = reparented_container_entity.get_parent_id() has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString() assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity" + current_instance_prefab = Prefab.get_prefab(self.prefab_file_name) + current_instance_prefab.instances.remove(self) + self.container_entity = reparented_container_entity + current_instance_prefab.instances.add(self) # This is a helper class which contains some of the useful information about a prefab template. class Prefab: @@ -117,31 +147,32 @@ class Prefab: self.file_path: str = get_prefab_file_path(file_path) self.instances: set[PrefabInstance] = set() - """ - Check if a prefab is ready to be used to generate its instances. - :param file_path: A unique file path of the target prefab. - :return: Whether the target prefab is loaded or not. - """ @classmethod def is_prefab_loaded(cls, file_path: str) -> bool: + """ + Check if a prefab is ready to be used to generate its instances. + :param file_path: A unique file path of the target prefab. + :return: Whether the target prefab is loaded or not. + """ return file_path in Prefab.existing_prefabs - """ - Check if a prefab exists in the directory for files of prefab tests. - :param file_name: A unique file name of the target prefab. - :return: Whether the target prefab exists or not. - """ + @classmethod def prefab_exists(cls, file_path: str) -> bool: + """ + Check if a prefab exists in the directory for files of prefab tests. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab exists or not. + """ return path.exists(get_prefab_file_path(file_path)) - """ - Return a prefab which can be used immediately. - :param file_name: A unique file name of the target prefab. - :return: The prefab with given file name. - """ @classmethod def get_prefab(cls, file_name: str) -> Prefab: + """ + Return a prefab which can be used immediately. + :param file_name: A unique file name of the target prefab. + :return: The prefab with given file name. + """ assert file_name, "Received an empty file_name" if Prefab.is_prefab_loaded(file_name): return Prefab.existing_prefabs[file_name] @@ -151,15 +182,15 @@ class Prefab: Prefab.existing_prefabs[file_name] = Prefab(file_name) return new_prefab - """ - Create a prefab in memory and return it. The very first instance of this prefab will also be created. - :param entities: The entities that should form the new prefab (along with their descendants). - :param file_name: A unique file name of new prefab. - :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. - :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. - """ @classmethod def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance): + """ + Create a prefab in memory and return it. The very first instance of this prefab will also be created. + :param entities: The entities that should form the new prefab (along with their descendants). + :param file_name: A unique file name of new prefab. + :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. + :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. + """ assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" new_prefab = Prefab(file_name) @@ -169,6 +200,9 @@ class Prefab: container_entity_id = create_prefab_result.GetValue() container_entity = EditorEntity(container_entity_id) + children_entity_ids = container_entity.get_children_ids() + + assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities." if prefab_instance_name: container_entity.set_name(prefab_instance_name) @@ -180,12 +214,12 @@ class Prefab: Prefab.existing_prefabs[file_name] = new_prefab return new_prefab, new_prefab_instance - """ - Remove target prefab instances. - :param prefab_instances: Instances to be removed. - """ @classmethod def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Remove target prefab instances. + :param prefab_instances: Instances to be removed. + """ entity_ids_to_remove = [] entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances] while entity_id_queue: @@ -212,15 +246,89 @@ class Prefab: instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) instance_deleted_prefab.instances.remove(instance) instance = PrefabInstance() - - """ - Instantiate an instance of this prefab. - :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. - :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. - :param prefab_position: The position in world space the prefab should be instantiated in. - :return: Instantiated PrefabInstance object owned by this prefab. - """ + + @classmethod + def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Duplicate target prefab instances. + :param prefab_instances: Instances to be duplicated. + :return: PrefabInstance objects of given prefab instances' duplicates. + """ + assert prefab_instances, "Input list of prefab instances should *not* be empty." + + common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id()) + common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + + duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids) + assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}" + + wait_for_propagation() + + duplicate_container_entity_ids = duplicate_prefab_result.GetValue() + common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \ + "Provided prefab instances are *not* the children of their common parent anymore after duplication." + assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \ + "Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication." + assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \ + "The children count of the given prefab instances' common parent entity is *not* increased to the expected number." + assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \ + "Provided prefab instances' parent should be the same as duplicates' parent." + + duplicate_instances = [] + for duplicate_container_entity_id in duplicate_container_entity_ids: + prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id) + assert prefab_file_path, "Returned file path should *not* be empty." + + prefab_file_name = Path(prefab_file_path).stem + duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name) + duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id)) + duplicate_instance_prefab.instances.add(duplicate_instance) + duplicate_instances.append(duplicate_instance) + + return duplicate_instances + + @classmethod + def detach_prefab(cls, prefab_instance: PrefabInstance): + """ + Detach target prefab instance. + :param prefab_instances: Instance to be detached. + """ + parent = EditorEntity(prefab_instance.container_entity.get_parent_id()) + parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment." + + detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id) + assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}" + + assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment." + + parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \ + "Target prefab instance's container entity id should still exists after the detachment and before the propagation." + + assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \ + "Parent entity should still keep the same amount of children entities." + + wait_for_propagation() + + instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name) + instance_owner_prefab.instances.remove(prefab_instance) + prefab_instance = PrefabInstance() + def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: + """ + Instantiate an instance of this prefab. + :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. + :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. + :param prefab_position: The position in world space the prefab should be instantiated in. + :return: Instantiated PrefabInstance object owned by this prefab. + """ parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() instantiate_prefab_result = prefab.PrefabPublicRequestBus( @@ -240,4 +348,6 @@ class Prefab: assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." self.instances.add(new_prefab_instance) + assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position." + return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py index 30a0055d5a..5337f0669c 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py @@ -47,3 +47,11 @@ class TestAutomation(TestAutomationBase): def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py index 9ae4614d80..bbebd70e04 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py @@ -16,16 +16,15 @@ def PrefabBasicWorkflow_CreateAndDeletePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Asserts if prefab creation doesn't succeeds + # Creates a prefab from the new entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Asserts if prefab deletion fails + # Deletes the prefab instance Prefab.remove_prefabs([car]) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py new file mode 100644 index 0000000000..2479ae549e --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py @@ -0,0 +1,32 @@ +""" +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 +""" + +def PrefabBasicWorkflow_CreateAndDuplicatePrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the new entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Duplicates the prefab instance + Prefab.duplicate_prefabs([car]) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py index f78bbf483d..1cbc591c29 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py @@ -22,24 +22,23 @@ def PrefabBasicWorkflow_CreateAndReparentPrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new car entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the car entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Creates another new Entity at the root level + # Creates another new wheel entity at the root level wheel_entity = EditorEntity.create_editor_entity() wheel_prefab_entities = [wheel_entity] - # Checks for wheel prefab creation passed or not + # Creates another prefab from the wheel entity _, wheel = Prefab.create_prefab( wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) - # Checks for prefab reparenting passed or not + # Reparents the wheel prefab instance to the container entity of the car prefab instance await wheel.ui_reparent_prefab_instance(car.container_entity.id) run_test() diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py index 568a2c15b4..cae105a9a9 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py @@ -17,12 +17,11 @@ def PrefabBasicWorkflow_CreatePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the new entity Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py new file mode 100644 index 0000000000..bdf77c4bf3 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py @@ -0,0 +1,51 @@ +""" +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 +""" + +def PrefabBasicWorkflow_CreateReparentAndDetachPrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' + + import editor_python_test_tools.pyside_utils as pyside_utils + + @pyside_utils.wrap_async + async def run_test(): + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new car entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the car entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Creates another new wheel entity at the root level + wheel_entity = EditorEntity.create_editor_entity() + wheel_prefab_entities = [wheel_entity] + + # Creates another prefab from the wheel entity + _, wheel = Prefab.create_prefab( + wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) + + # Reparents the wheel prefab instance to the container entity of the car prefab instance + await wheel.ui_reparent_prefab_instance(car.container_entity.id) + + # Detaches the wheel prefab instance + Prefab.detach_prefab(wheel) + + run_test() + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py index 1b962d2ca7..a701802cd4 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py @@ -19,9 +19,8 @@ def PrefabBasicWorkflow_InstantiatePrefab(): prefab_test_utils.open_base_tests_level() - # Checks for prefab instantiation passed or not + # Instantiates a new car prefab instance test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME) - test_instance = test_prefab.instantiate( prefab_position=INSTANTIATED_TEST_PREFAB_POSITION) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py index f82af23023..f865daf41a 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py @@ -18,20 +18,6 @@ import azlmbr.components as components import azlmbr.entity as entity import azlmbr.legacy.general as general -def check_entity_at_position(entity_id, expected_entity_position): - entity_at_expected_position_result = ( - "entity is at expected position", - "entity is *not* at expected position") - - actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id) - is_at_position = actual_entity_position.IsClose(expected_entity_position) - Report.result(entity_at_expected_position_result, is_at_position) - - if not is_at_position: - Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}") - - return is_at_position - def check_entity_children_count(entity_id, expected_children_count): entity_children_count_matched_result = ( "Entity with a unique name found", @@ -47,19 +33,6 @@ def check_entity_children_count(entity_id, expected_children_count): return entity_children_count_matched -def get_children_ids_by_name(entity_id, entity_name): - entity = EditorEntity(entity_id) - children_entity_ids = entity.get_children_ids() - - result = [] - for child_entity_id in children_entity_ids: - child_entity = EditorEntity(child_entity_id) - child_entity_name = child_entity.get_name() - if child_entity_name == entity_name: - result.append(child_entity_id) - - return result - def open_base_tests_level(): helper.init_idle() helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py index 0461ff2647..1c3652cae9 100644 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py @@ -9,6 +9,7 @@ import os import pytest import ly_test_tools.environment.file_system as file_system +import ly_test_tools._internal.pytest_plugin as internal_plugin from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite @@ -79,6 +80,8 @@ class TestAutomation(EditorTestSuite): class test_LandscapeCanvas_GradientNodes_EntityRemovedOnNodeDelete(EditorSharedTest): from .EditorScripts import GradientNodes_EntityRemovedOnNodeDelete as test_module + @pytest.mark.skipif("debug" == os.path.basename(internal_plugin.build_directory), + reason="https://github.com/o3de/o3de/issues/4872") class test_LandscapeCanvas_GraphUpdates_UpdateComponents(EditorSharedTest): from .EditorScripts import GraphUpdates_UpdateComponents as test_module diff --git a/Code/Editor/Controls/ColorGradientCtrl.cpp b/Code/Editor/Controls/ColorGradientCtrl.cpp deleted file mode 100644 index 446e5810c5..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.cpp +++ /dev/null @@ -1,923 +0,0 @@ -/* - * 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 "EditorDefs.h" - -#include "ColorGradientCtrl.h" - -// Qt -#include -#include - -// AzQtComponents -#include - - -#define MIN_TIME_EPSILON 0.01f - -////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::CColorGradientCtrl(QWidget* parent) - : QWidget(parent) -{ - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - m_nKeyDrawRadius = 3; - m_bTracking = false; - m_pSpline = nullptr; - m_fMinTime = -1; - m_fMaxTime = 1; - m_fMinValue = -1; - m_fMaxValue = 1; - m_fTooltipScaleX = 1; - m_fTooltipScaleY = 1; - m_bNoTimeMarker = true; - m_bLockFirstLastKey = false; - m_bNoZoom = true; - - ClearSelection(); - - m_bSelectedKeys.reserve(0); - - m_fTimeMarker = -10; - - m_grid.zoom.x = 100; - - setMouseTracking(true); -} - -CColorGradientCtrl::~CColorGradientCtrl() -{ -} - - -///////////////////////////////////////////////////////////////////////////// -// QColorGradientCtrl message handlers - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::resizeEvent(QResizeEvent* event) -{ - QWidget::resizeEvent(event); - - QRect rc(QPoint(0, 0), event->size()); - m_rcGradient = rc; - m_rcGradient.setHeight(m_rcGradient.height() - 11); - //m_rcGradient.DeflateRect(4,4); - - m_grid.rect = m_rcGradient; - if (m_bNoZoom) - { - m_grid.zoom.x = static_cast(m_grid.rect.width()); - } - - m_rcKeys = rc; - m_rcKeys.setTop(m_rcKeys.bottom() - 10); -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetZoom(float fZoom) -{ - m_grid.zoom.x = fZoom; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetOrigin(float fOffset) -{ - m_grid.origin.x = fOffset; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::KeyToPoint(int nKey) -{ - if (nKey >= 0) - { - return TimeToPoint(m_pSpline->GetKeyTime(nKey)); - } - return QPoint(0, 0); -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::TimeToPoint(float time) -{ - return QPoint(m_grid.WorldToClient(Vec2(time, 0)).x(), m_rcGradient.height() / 2); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::TimeToColor(float time) -{ - ISplineInterpolator::ValueType val; - m_pSpline->Interpolate(time, val); - const AZ::Color col = ValueToColor(val); - return col; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val) -{ - time = XOfsToTime(point.x()); - ColorToValue(TimeToColor(time), val); -} - -////////////////////////////////////////////////////////////////////////// -float CColorGradientCtrl::XOfsToTime(int x) -{ - return m_grid.ClientToWorld(QPoint(x, 0)).x; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::XOfsToPoint(int x) -{ - return TimeToPoint(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::XOfsToColor(int x) -{ - return TimeToColor(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::paintEvent(QPaintEvent* e) -{ - QPainter painter(this); - - QRect rcClient = rect(); - - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - { - if (!isEnabled()) - { - painter.setBrush(palette().button()); - painter.drawRect(rcClient); - return; - } - - ////////////////////////////////////////////////////////////////////////// - // Fill keys backgound. - ////////////////////////////////////////////////////////////////////////// - QRect rcKeys = m_rcKeys.intersected(e->rect()); - painter.setBrush(palette().button()); - painter.drawRect(rcKeys); - ////////////////////////////////////////////////////////////////////////// - - //Draw Keys and Curve - if (m_pSpline) - { - DrawGradient(e, &painter); - DrawKeys(e, &painter); - } - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawGradient(QPaintEvent* e, QPainter* painter) -{ - //Draw Curve - // create and select a thick, white pen - painter->setPen(QPen(QColor(128, 255, 128), 1, Qt::SolidLine)); - - const QRect rcClip = e->rect().intersected(m_rcGradient); - const int right = rcClip.left() + rcClip.width(); - for (int x = rcClip.left(); x < right; x++) - { - const AZ::Color col = XOfsToColor(x); - QPen pen(QColor(col.GetR8(), col.GetG8(), col.GetR8(), col.GetA8()), 1, Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(x, m_rcGradient.top(), x, m_rcGradient.top() + m_rcGradient.height()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawKeys(QPaintEvent* e, QPainter* painter) -{ - if (!m_pSpline) - { - return; - } - - // create and select a white pen - painter->setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine)); - - QRect rcClip = e->rect(); - - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - float time = m_pSpline->GetKeyTime(i); - QPoint pt = TimeToPoint(time); - - if (pt.x() < rcClip.left() - 8 || pt.x() > rcClip.left() + rcClip.width() + 8) - { - continue; - } - - const AZ::Color clr = TimeToColor(time); - QBrush brush(QColor(clr.GetR8(), clr.GetG8(), clr.GetB8(), clr.GetA8())); - painter->setBrush(brush); - - // Find the midpoints of the top, right, left, and bottom - // of the client area. They will be the vertices of our polygon. - QPoint pts[3]; - pts[0].rx() = pt.x(); - pts[0].ry() = m_rcKeys.top() + 1; - pts[1].rx() = pt.x() - 5; - pts[1].ry() = m_rcKeys.top() + 8; - pts[2].rx() = pt.x() + 5; - pts[2].ry() = m_rcKeys.top() + 8; - painter->drawPolygon(pts, 3); - - if (m_bSelectedKeys[i]) - { - QPen pen(QColor(200, 0, 0), 1, Qt::SolidLine); - QPen oldPen = painter->pen(); - painter->setPen(pen); - painter->drawPolygon(pts, 3); - painter->setPen(oldPen); - } - } - - if (!m_bNoTimeMarker) - { - QPen timePen(QColor(255, 0, 255), 1, Qt::SolidLine); - painter->setPen(timePen); - QPoint pt = TimeToPoint(m_fTimeMarker); - painter->drawLine(pt.x(), m_rcGradient.top() + 1, pt.x(), m_rcGradient.bottom() - 1); - } -} - -void CColorGradientCtrl::UpdateTooltip(QPoint pos) -{ - if (m_nHitKeyIndex >= 0) - { - float time = m_pSpline->GetKeyTime(m_nHitKeyIndex); - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(m_nHitKeyIndex, val); - - AZ::Color col = TimeToColor(time); - int cont_s = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_IN_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - int cont_d = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_OUT_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - - QString tipText(tr("%1 : %2,%3,%4 [%5,%6]").arg(time * m_fTooltipScaleX, 0, 'f', 2).arg(col.GetR8()).arg(col.GetG8()).arg(col.GetB8()).arg(cont_s).arg(cont_d)); - const QPoint globalPos = mapToGlobal(pos); - QToolTip::showText(mapToGlobal(pos), tipText, this, QRect(globalPos, QSize(1, 1))); - } -} - -///////////////////////////////////////////////////////////////////////////// -//Mouse Message Handlers -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mousePressEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonDown(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonDown(event); - } -} - -void CColorGradientCtrl::OnLButtonDown([[maybe_unused]] QMouseEvent* event) -{ - if (m_bTracking) - { - return; - } - if (!m_pSpline) - { - return; - } - - setFocus(); - - switch (m_hitCode) - { - case HIT_KEY: - StartTracking(); - SetActiveKey(m_nHitKeyIndex); - break; - - /* - case HIT_SPLINE: - { - // Cycle the spline slope of the nearest key. - int flags = m_pSpline->GetKeyFlags(m_nHitKeyIndex); - if (m_nHitKeyDist < 0) - // Toggle left side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_IN_SHIFT; - if (m_nHitKeyDist > 0) - // Toggle right side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_OUT_SHIFT; - m_pSpline->SetKeyFlags(m_nHitKeyIndex, flags); - m_pSpline->Update(); - - SetActiveKey(-1); - SendNotifyEvent( CLRGRDN_CHANGE ); - if (m_updateCallback) - m_updateCallback(this); - break; - } - */ - - case HIT_NOTHING: - SetActiveKey(-1); - break; - } - update(); -} - - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonDown([[maybe_unused]] QMouseEvent* event) -{ -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseDoubleClickEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (event->button() != Qt::LeftButton) - { - return; - } - - switch (m_hitCode) - { - case HIT_SPLINE: - { - int iIndex = InsertKey(event->pos()); - SetActiveKey(iIndex); - EditKey(iIndex); - - update(); - } - break; - case HIT_KEY: - { - EditKey(m_nHitKeyIndex); - } - break; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseMoveEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (!m_bTracking) - { - switch (HitTest(event->pos())) - { - case HIT_SPLINE: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRWHITE)); - } break; - case HIT_KEY: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCK)); - } break; - default: - break; - } - } - - if (m_bTracking) - { - TrackKey(event->pos()); - } - - if (m_bTracking || m_nHitKeyIndex >= 0) - { - UpdateTooltip(event->pos()); - } - else - { - QToolTip::hideText(); - } -} - -void CColorGradientCtrl::mouseReleaseEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonUp(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonUp(event); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnLButtonUp(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (m_bTracking) - { - StopTracking(event->pos()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonUp([[maybe_unused]] QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetActiveKey(int nIndex) -{ - ClearSelection(); - - //Activate New Key - if (nIndex >= 0) - { - m_bSelectedKeys[nIndex] = true; - } - m_nActiveKey = nIndex; - update(); - - SendNotifyEvent(CLRGRDN_ACTIVE_KEY_CHANGE); -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetSpline(ISplineInterpolator* pSpline, bool bRedraw) -{ - if (pSpline != m_pSpline) - { - //if (pSpline && pSpline->GetNumDimensions() != 3) - //return; - m_pSpline = pSpline; - m_nActiveKey = -1; - } - - ClearSelection(); - - if (bRedraw) - { - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -ISplineInterpolator* CColorGradientCtrl::GetSpline() -{ - return m_pSpline; -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::keyPressEvent(QKeyEvent* event) -{ - bool bProcessed = false; - - if (m_nActiveKey != -1 && m_pSpline) - { - switch (event->key()) - { - case Qt::Key_Delete: - { - RemoveKey(m_nActiveKey); - bProcessed = true; - } break; - case Qt::Key_Up: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Down: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Left: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Right: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - - default: - break; //do nothing - } - - update(); - } - - event->setAccepted(bProcessed); -} - -////////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::EHitCode CColorGradientCtrl::HitTest(QPoint point) -{ - if (!m_pSpline) - { - return HIT_NOTHING; - } - - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - QRect rc = rect(); - - m_nHitKeyIndex = -1; - - if (rc.contains(point)) - { - m_nHitKeyDist = 0xFFFF; - m_hitCode = HIT_SPLINE; - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - QPoint splinePt = TimeToPoint(m_pSpline->GetKeyTime(i)); - if (abs(point.x() - splinePt.x()) < abs(m_nHitKeyDist)) - { - m_nHitKeyIndex = i; - m_nHitKeyDist = point.x() - splinePt.x(); - } - } - if (abs(m_nHitKeyDist) < 4) - { - m_hitCode = HIT_KEY; - } - } - else - { - m_hitCode = HIT_NOTHING; - } - - return m_hitCode; -} - -/////////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StartTracking() -{ - m_bTracking = true; - - GetIEditor()->BeginUndo(); - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCKCROSS)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::TrackKey(QPoint point) -{ - if (point.x() < m_rcGradient.left() || point.y() > m_rcGradient.right()) - { - return; - } - - int nKey = m_nHitKeyIndex; - - if (nKey >= 0) - { - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - // Clamp to min/max time. - if (time < m_fMinTime || time > m_fMaxTime) - { - return; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Switch to next key. - if ((m_pSpline->GetKeyTime(i) < time && i > nKey) || - (m_pSpline->GetKeyTime(i) > time && i < nKey)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - SetActiveKey(i); - m_nHitKeyIndex = i; - return; - } - } - - if (!m_bLockFirstLastKey || (nKey != 0 && nKey != m_pSpline->GetKeyCount() - 1)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - } - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StopTracking(QPoint point) -{ - if (!m_bTracking) - { - return; - } - - GetIEditor()->AcceptUndo("Spline Move"); - - if (m_nHitKeyIndex >= 0) - { - QRect rc = rect(); - rc = rc.marginsAdded(QMargins(100, 100, 100, 100)); - if (!rc.contains(point)) - { - RemoveKey(m_nHitKeyIndex); - } - } - - m_bTracking = false; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::EditKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - SetActiveKey(nKey); - - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(nKey, val); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB); - dlg.setCurrentColor(ValueToColor(val)); - dlg.setSelectedColor(ValueToColor(val)); - connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CColorGradientCtrl::OnKeyColorChanged); - if (dlg.exec() == QDialog::Accepted) - { - CUndo undo("Modify Gradient Color"); - OnKeyColorChanged(dlg.selectedColor()); - } - else - { - OnKeyColorChanged(ValueToColor(val)); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnKeyColorChanged(const AZ::Color& color) -{ - int nKey = m_nActiveKey; - if (!m_pSpline) - { - return; - } - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - ISplineInterpolator::ValueType val; - ColorToValue(color, val); - m_pSpline->SetKeyValue(nKey, val); - update(); - - if (m_bLockFirstLastKey) - { - if (nKey == 0) - { - m_pSpline->SetKeyValue(m_pSpline->GetKeyCount() - 1, val); - } - else if (nKey == m_pSpline->GetKeyCount() - 1) - { - m_pSpline->SetKeyValue(0, val); - } - } - m_pSpline->Update(); - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - GetIEditor()->UpdateViews(eRedrawViewports); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::RemoveKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - if (m_bLockFirstLastKey) - { - if (nKey == 0 || nKey == m_pSpline->GetKeyCount() - 1) - { - return; - } - } - - CUndo undo("Remove Spline Key"); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - if (m_pSpline) - { - m_pSpline->RemoveKey(nKey); - m_pSpline->Update(); - } - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); -} - -////////////////////////////////////////////////////////////////////////// -int CColorGradientCtrl::InsertKey(QPoint point) -{ - CUndo undo("Spline Insert Key"); - - ISplineInterpolator::ValueType val; - - float time; - PointToTimeValue(point, time, val); - - if (time < m_fMinTime || time > m_fMaxTime) - { - return -1; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Skip if any key already have time that is very close. - if (fabs(m_pSpline->GetKeyTime(i) - time) < MIN_TIME_EPSILON) - { - return i; - } - } - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - m_pSpline->InsertKey(time, val); - m_pSpline->Interpolate(time, val); - ClearSelection(); - update(); - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Find key with added time. - if (m_pSpline->GetKeyTime(i) == time) - { - return i; - } - } - - return -1; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ClearSelection() -{ - m_nActiveKey = -1; - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - for (int i = 0; i < (int)m_bSelectedKeys.size(); i++) - { - m_bSelectedKeys[i] = false; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetTimeMarker(float fTime) -{ - if (!m_pSpline) - { - return; - } - - { - QPoint pt = TimeToPoint(m_fTimeMarker); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - { - QPoint pt = TimeToPoint(fTime); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - m_fTimeMarker = fTime; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SendNotifyEvent(int nEvent) -{ - switch (nEvent) - { - case CLRGRDN_BEFORE_CHANGE: - emit beforeChange(); - break; - case CLRGRDN_CHANGE: - emit change(); - break; - case CLRGRDN_ACTIVE_KEY_CHANGE: - emit activeKeyChange(); - break; - } -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::ValueToColor(ISplineInterpolator::ValueType val) -{ - const AZ::Color color(val[0], val[1], val[2], 1.0); - return color.LinearToGamma(); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val) -{ - const AZ::Color colLin = col.GammaToLinear(); - val[0] = colLin.GetR(); - val[1] = colLin.GetG(); - val[2] = colLin.GetB(); - val[3] = 0; -} - -void CColorGradientCtrl::SetNoTimeMarker(bool noTimeMarker) -{ - m_bNoTimeMarker = noTimeMarker; - update(); -} - - -#include diff --git a/Code/Editor/Controls/ColorGradientCtrl.h b/Code/Editor/Controls/ColorGradientCtrl.h deleted file mode 100644 index bb1a83b0c1..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.h +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 - * - */ - - -#ifndef CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#define CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#pragma once - -#if !defined(Q_MOC_RUN) -#include -#include -#include "Controls/WndGridHelper.h" -#endif - -namespace AZ -{ - class Color; -} - -// Notify event sent when spline is being modified. -#define CLRGRDN_CHANGE (0x0001) -// Notify event sent just before when spline is modified. -#define CLRGRDN_BEFORE_CHANGE (0x0002) -// Notify event sent when the active key changes -#define CLRGRDN_ACTIVE_KEY_CHANGE (0x0003) - -////////////////////////////////////////////////////////////////////////// -// Spline control. -////////////////////////////////////////////////////////////////////////// -class CColorGradientCtrl - : public QWidget -{ - Q_OBJECT -public: - CColorGradientCtrl(QWidget* parent = nullptr); - virtual ~CColorGradientCtrl(); - - //Key functions - int GetActiveKey() { return m_nActiveKey; }; - void SetActiveKey(int nIndex); - int InsertKey(QPoint point); - - // Turns on/off zooming and scroll support. - void SetNoZoom([[maybe_unused]] bool bNoZoom) { m_bNoZoom = false; }; - - void SetTimeRange(float tmin, float tmax) { m_fMinTime = tmin; m_fMaxTime = tmax; } - void SetValueRange(float tmin, float tmax) { m_fMinValue = tmin; m_fMaxValue = tmax; } - void SetTooltipValueScale(float x, float y) { m_fTooltipScaleX = x; m_fTooltipScaleY = y; }; - // Lock value of first and last key to be the same. - void LockFirstAndLastKeys(bool bLock) { m_bLockFirstLastKey = bLock; } - - void SetSpline(ISplineInterpolator* pSpline, bool bRedraw = false); - ISplineInterpolator* GetSpline(); - - void SetTimeMarker(float fTime); - - // Zoom in pixels per time unit. - void SetZoom(float fZoom); - void SetOrigin(float fOffset); - - typedef AZStd::function UpdateCallback; - void SetUpdateCallback(const UpdateCallback& cb) { m_updateCallback = cb; }; - - void SetNoTimeMarker(bool noTimeMarker); - -signals: - void change(); - void beforeChange(); - void activeKeyChange(); - -protected: - enum EHitCode - { - HIT_NOTHING, - HIT_KEY, - HIT_SPLINE, - }; - - void paintEvent(QPaintEvent* e) override; - void resizeEvent(QResizeEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - void OnLButtonDown(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event) override; - void OnLButtonUp(QMouseEvent* event); - void OnRButtonUp(QMouseEvent* event); - void mouseDoubleClickEvent(QMouseEvent* event) override; - void OnRButtonDown(QMouseEvent* event); - void keyPressEvent(QKeyEvent* event) override; - - // Drawing functions - void DrawGradient(QPaintEvent* e, QPainter* painter); - void DrawKeys(QPaintEvent* e, QPainter* painter); - void UpdateTooltip(QPoint pos); - - EHitCode HitTest(QPoint point); - - //Tracking support helper functions - void StartTracking(); - void TrackKey(QPoint point); - void StopTracking(QPoint point); - void RemoveKey(int nKey); - void EditKey(int nKey); - - QPoint KeyToPoint(int nKey); - QPoint TimeToPoint(float time); - void PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val); - float XOfsToTime(int x); - QPoint XOfsToPoint(int x); - - AZ::Color XOfsToColor(int x); - AZ::Color TimeToColor(float time); - - void ClearSelection(); - - void SendNotifyEvent(int nEvent); - - AZ::Color ValueToColor(ISplineInterpolator::ValueType val); - void ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val); - - -private: - void OnKeyColorChanged(const AZ::Color& color); - -private: - ISplineInterpolator* m_pSpline; - - bool m_bNoZoom; - - QRect m_rcClipRect; - QRect m_rcGradient; - QRect m_rcKeys; - - QPoint m_hitPoint; - EHitCode m_hitCode; - int m_nHitKeyIndex; - int m_nHitKeyDist; - QPoint m_curvePoint; - - float m_fTimeMarker; - - int m_nActiveKey; - int m_nKeyDrawRadius; - - bool m_bTracking; - - float m_fMinTime, m_fMaxTime; - float m_fMinValue, m_fMaxValue; - float m_fTooltipScaleX, m_fTooltipScaleY; - - bool m_bLockFirstLastKey; - - bool m_bNoTimeMarker; - - std::vector m_bSelectedKeys; - - UpdateCallback m_updateCallback; - - CWndGridHelper m_grid; -}; - -#endif // CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp index c228fbda09..68c4acb95e 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp @@ -27,7 +27,6 @@ void RegisterReflectedVarHandlers() EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LocalStringPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LightAnimationPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew UserPopupWidgetHandler()); - EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew ColorCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew FloatCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew MotionPropertyWidgetHandler()); } diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp index b9f7e12e95..2278f55d95 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp @@ -170,30 +170,3 @@ bool FloatCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CSpline GUI->SetSpline(reinterpret_cast(instance.m_spline)); return false; } - - -QWidget* ColorCurveHandler::CreateGUI(QWidget *pParent) -{ - CColorGradientCtrl* gradientCtrl = new CColorGradientCtrl(pParent); - //connect(gradientCtrl, &CColorGradientCtrl::change, [gradientCtrl]() - //{ - // EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, gradientCtrl); - //}); - gradientCtrl->SetTimeRange(0, 1); - gradientCtrl->setFixedHeight(36); - return gradientCtrl; - -} - -void ColorCurveHandler::ConsumeAttribute(CColorGradientCtrl*, AZ::u32, AzToolsFramework::PropertyAttributeReader*, const char*) -{} - -void ColorCurveHandler::WriteGUIValuesIntoProperty([[maybe_unused]] size_t index, [[maybe_unused]] CColorGradientCtrl* GUI, [[maybe_unused]] property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{} - -bool ColorCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CColorGradientCtrl* GUI, const property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{ - GUI->SetSpline(reinterpret_cast(instance.m_spline)); - return false; -} - diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h index e63a974c91..5ec24b679d 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h @@ -16,7 +16,6 @@ #include #include "ReflectedVar.h" #include "Util/VariablePropertyType.h" -#include "Controls/ColorGradientCtrl.h" #include "Controls/SplineCtrl.h" #include #endif @@ -82,17 +81,4 @@ public: void OnSplineChange(CSplineCtrl*); }; -class ColorCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CColorGradientCtrl> -{ -public: - AZ_CLASS_ALLOCATOR(ColorCurveHandler, AZ::SystemAllocator, 0); - bool IsDefaultHandler() const override { return false; } - QWidget* CreateGUI(QWidget *pParent) override; - - AZ::u32 GetHandlerName(void) const override { return AZ_CRC("ePropertyColorCurve", 0xa30da4ec); } - - void ConsumeAttribute(CColorGradientCtrl* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName) override; - void WriteGUIValuesIntoProperty(size_t index, CColorGradientCtrl* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node) override; - bool ReadValuesIntoGUI(size_t index, CColorGradientCtrl* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node) override; -}; #endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H diff --git a/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp b/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp index 0b514513ca..d05074c483 100644 --- a/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp +++ b/Code/Editor/Plugins/EditorCommon/SaveUtilities/AsyncSaveRunner.cpp @@ -76,6 +76,7 @@ namespace AZ else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_ProviderIsDown) { message = "Failed to put entries/dependencies into source control as the provider is not available.\n"; + reportAsWarning = true; } else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_CertificateInvalid) { diff --git a/Code/Editor/editor_lib_files.cmake b/Code/Editor/editor_lib_files.cmake index 031ad26d76..47b69765ba 100644 --- a/Code/Editor/editor_lib_files.cmake +++ b/Code/Editor/editor_lib_files.cmake @@ -330,8 +330,6 @@ set(FILES Commands/CommandManager.h Controls/BitmapToolTip.cpp Controls/BitmapToolTip.h - Controls/ColorGradientCtrl.cpp - Controls/ColorGradientCtrl.h Controls/ConsoleSCB.cpp Controls/ConsoleSCB.h Controls/ConsoleSCB.ui diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp new file mode 100644 index 0000000000..2c856fb4b0 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp @@ -0,0 +1,254 @@ +/* + * 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 +#include +#include + +namespace AZ +{ + JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element) + { + using namespace JsonSerializationResult; + + for (auto& path : importPathStack) + { + if (importPath == path) + { + return settings.m_reporting( + AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()), + ResultCode(Tasks::Import, Outcomes::Catastrophic), element); + } + } + + importPathStack.push_back(importPath); + AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer); + JsonImportSettings nestedImportSettings; + nestedImportSettings.m_importer = settings.m_importer; + nestedImportSettings.m_reporting = settings.m_reporting; + nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies; + ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement); + importPathStack.pop_back(); + + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject()) + { + for (auto& field : jsonDoc.GetObject()) + { + if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0) + { + const rapidjson::Value& importDirective = field.value; + AZ::IO::FixedMaxPath importAbsPath = importPathStack.back(); + importAbsPath.RemoveFilename(); + AZStd::string importName; + if (importDirective.IsObject()) + { + auto filenameField = importDirective.FindMember("filename"); + if (filenameField != importDirective.MemberEnd()) + { + importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength()); + } + } + else + { + importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength()); + } + importAbsPath.Append(importName); + + rapidjson::Value patch; + ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports) + { + rapidjson::Pointer path(element.Get().data(), element.Get().size()); + settings.m_importer->AddImportDirective(path, importName); + } + if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies) + { + settings.m_importer->AddImportedFile(importAbsPath.String()); + } + + ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + settings.m_importer->ApplyPatch(jsonDoc, patch, allocator); + } + else if (field.value.IsObject() || field.value.IsArray()) + { + ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength())); + ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + } + else if(jsonDoc.IsArray()) + { + int index = 0; + for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index) + { + if (!elem->IsObject() && !elem->IsArray()) + { + continue; + } + ScopedStackedString entryName(element, index); + ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject() || jsonDoc.IsArray()) + { + const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives(); + for (auto& import : importDirectives) + { + rapidjson::Pointer importPtr = import.first; + rapidjson::Value* currentValue = importPtr.Get(jsonDoc); + + rapidjson::Value importedValue(rapidjson::kObjectType); + importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator); + ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + rapidjson::Value patch; + settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator); + settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second); + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native()); + if (importedObject.IsSuccess()) + { + rapidjson::Value& importedDoc = importedObject.GetValue(); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + } + else + { + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename) + { + using namespace JsonSerializationResult; + + importPtr->SetObject(); + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + rapidjson::Value importDirective(rapidjson::kObjectType); + importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator); + importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator); + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator); + } + else + { + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator) + { + return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch); + } + + void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile) + { + m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile)); + } + + void BaseJsonImporter::AddImportedFile(AZStd::string importedFile) + { + m_importedFiles.insert(AZStd::move(importedFile)); + } + + const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives() + { + return m_importDirectives; + } + + const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles() + { + return m_importedFiles; + } +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h new file mode 100644 index 0000000000..ef53e265d8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h @@ -0,0 +1,108 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + struct JsonImportSettings; + + class BaseJsonImporter + { + public: + AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}"); + + using ImportDirectivesList = AZStd::vector>; + using ImportedFilesList = AZStd::unordered_set; + + virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, + const AZStd::string& importFilename); + + virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator); + + void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile); + const ImportDirectivesList& GetImportDirectives(); + + void AddImportedFile(AZStd::string importedFile); + const ImportedFilesList& GetImportedFiles(); + + virtual ~BaseJsonImporter() = default; + + protected: + + ImportDirectivesList m_importDirectives; + ImportedFilesList m_importedFiles; + }; + + enum class ImportTracking : AZ::u8 + { + None = 0, + Dependencies = (1<<0), + Imports = (1<<1), + All = (Dependencies | Imports) + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking); + + class JsonImportResolver final + { + public: + + using ImportPathStack = AZStd::vector; + + JsonImportResolver() = delete; + JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete; + JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete; + JsonImportResolver(const JsonImportResolver& rhs) = delete; + JsonImportResolver(JsonImportResolver&& rhs) = delete; + ~JsonImportResolver() = delete; + + static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element); + + static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + private: + + static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element); + }; + + + struct JsonImportSettings final + { + JsonSerializationResult::JsonIssueCallback m_reporting; + + BaseJsonImporter* m_importer = nullptr; + + ImportTracking m_resolveFlags = ImportTracking::All; + + AZ::IO::FixedMaxPath m_loadedJsonPath; + }; +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp index 45549b8078..a9b2d2fefa 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp @@ -706,7 +706,7 @@ namespace AZ rapidjson::Value(rapidjson::kNullType), field.value, element, settings); } - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -717,6 +717,10 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } } // Do an extra pass to find all the fields that are removed. @@ -751,7 +755,7 @@ namespace AZ rapidjson::Value value; ResultCode result = CreateMergePatchInternal(value, allocator, rapidjson::Value(rapidjson::kNullType), field.value, element, settings); - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -762,11 +766,20 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } + } + + if (target.MemberCount() == 0) + { + resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.", + ResultCode(Tasks::CreatePatch, Outcomes::Success), element)); } } patch = AZStd::move(resultValue); - resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success)); return resultCode; } else diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp index bc07f684f6..db76e46f2b 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,11 +20,6 @@ namespace AZ { - const char* JsonSerialization::TypeIdFieldIdentifier = "$type"; - const char* JsonSerialization::DefaultStringIdentifier = "{}"; - const char* JsonSerialization::KeyFieldIdentifier = "Key"; - const char* JsonSerialization::ValueFieldIdentifier = "Value"; - namespace JsonSerializationInternal { template @@ -394,6 +390,60 @@ namespace AZ } } + JsonSerializationResult::ResultCode JsonSerialization::ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + JsonImportResolver::ImportPathStack importPathStack; + importPathStack.push_back(settings.m_loadedJsonPath); + StackedString element(StackedString::Format::JsonPointer); + + return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element); + } + + JsonSerializationResult::ResultCode JsonSerialization::RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + settings.m_resolveFlags = ImportTracking::None; + + return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings); + } + JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer, AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path) { diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h index c85847ac78..d961953a1d 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h @@ -18,6 +18,8 @@ namespace AZ { class BaseJsonSerializer; + + struct JsonImportSettings; enum class JsonMergeApproach { @@ -51,10 +53,11 @@ namespace AZ class JsonSerialization final { public: - static const char* TypeIdFieldIdentifier; - static const char* DefaultStringIdentifier; - static const char* KeyFieldIdentifier; - static const char* ValueFieldIdentifier; + static constexpr const char* TypeIdFieldIdentifier = "$type"; + static constexpr const char* DefaultStringIdentifier = "{}"; + static constexpr const char* KeyFieldIdentifier = "Key"; + static constexpr const char* ValueFieldIdentifier = "Value"; + static constexpr const char* ImportDirectiveIdentifier = "$import"; //! Merges two json values together by applying "patch" to "target" using the selected merge algorithm. //! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will @@ -284,6 +287,22 @@ namespace AZ //! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error". static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs); + //! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed + //! in through the settings. + //! @param jsonDoc The json document in which to resolve imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are resolved. + static JsonSerializationResult::ResultCode ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + //! Restores all import directives that were present in the json document. The same importer object that was + //! passed into ResolveImports through the settings needs to be passed here through settings as well. + //! @param jsonDoc The json document in which to restore imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are restored. + static JsonSerializationResult::ResultCode RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + private: JsonSerialization() = delete; ~JsonSerialization() = delete; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp index 7e84aced7b..822c1c43d5 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp @@ -69,6 +69,9 @@ namespace AZ case Tasks::CreatePatch: target.append("a create patch operation "); break; + case Tasks::Import: + target.append("an import operation"); + break; default: target.append("an unknown operation "); break; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h index 8590971a1c..204c40b8ca 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h @@ -32,7 +32,8 @@ namespace AZ ReadField, //!< Task to read a field from JSON to a value. WriteValue, //!< Task to write a value to a JSON field. Merge, //!< Task to merge two JSON values/documents together. - CreatePatch //!< Task to create a patch to transform one value/document to another. + CreatePatch, //!< Task to create a patch to transform one value/document to another. + Import //!< Task to import a JSON document. }; //! Describes how the task was processed. diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index 4d95ddf098..41229429f2 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -522,6 +522,8 @@ set(FILES Serialization/Json/IntSerializer.cpp Serialization/Json/JsonDeserializer.h Serialization/Json/JsonDeserializer.cpp + Serialization/Json/JsonImporter.cpp + Serialization/Json/JsonImporter.h Serialization/Json/JsonMerger.h Serialization/Json/JsonMerger.cpp Serialization/Json/JsonSerialization.h diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp new file mode 100644 index 0000000000..5121efa5e3 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp @@ -0,0 +1,413 @@ +/* + * 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 +#include +#include + +namespace JsonSerializationTests +{ + class JsonImportingTests; + + class JsonImporterCustom + : public AZ::BaseJsonImporter + { + public: + AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}"); + + + AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override; + + JsonImporterCustom(JsonImportingTests* tests) + { + testClass = tests; + } + + private: + JsonImportingTests* testClass; + }; + + class JsonImportingTests + : public BaseJsonSerializerFixture + { + public: + void SetUp() override + { + BaseJsonSerializerFixture::SetUp(); + } + + void TearDown() override + { + BaseJsonSerializerFixture::TearDown(); + } + + void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out) + { + const char *objectJson = R"({ + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + })"; + + const char *arrayJson = R"([ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ])"; + + const char *nestedImportJson = R"({ + "desc" : "Nested Import", + "obj" : {"$import" : "object.json"} + })"; + + const char *nestedImportCycle1Json = R"({ + "desc" : "Nested Import Cycle 1", + "obj" : {"$import" : "nested_import_c2.json"} + })"; + + const char *nestedImportCycle2Json = R"({ + "desc" : "Nested Import Cycle 2", + "obj" : {"$import" : "nested_import_c1.json"} + })"; + + if (docName.compare("object.json") == 0) + { + out.Parse(objectJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("array.json") == 0) + { + out.Parse(arrayJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import.json") == 0) + { + out.Parse(nestedImportJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c1.json") == 0) + { + out.Parse(nestedImportCycle1Json); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c2.json") == 0) + { + out.Parse(nestedImportCycle2Json); + ASSERT_FALSE(out.HasParseError()); + } + } + + protected: + void TestImportLoadStore(const char* input, const char* expectedImportedValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + rapidjson::Document expectedOutcome; + expectedOutcome.Parse(expectedImportedValue); + ASSERT_FALSE(expectedOutcome.HasParseError()); + + TestResolveImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject()); + + rapidjson::Document originalInput; + originalInput.Parse(input); + ASSERT_FALSE(originalInput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestImportCycle(const char* input) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj); + + EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestInsertNewImport(const char* input, const char* expectedRestoredValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + TestResolveImports(importerObj); + + importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json"); + + rapidjson::Document expectedOutput; + expectedOutput.Parse(expectedRestoredValue); + ASSERT_FALSE(expectedOutput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + + AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + }; + + AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath, + rapidjson::Document::AllocatorType& allocator) + { + AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import); + + rapidjson::Document importedDoc; + testClass->GetTestDocument(importedFilePath.String(), importedDoc); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + + return resultCode; + } + + // Test Cases + + TEST_F(JsonImportingTests, ImportSimpleObjectTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": {"$import" : "object.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_2" : "patched_value" } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "patched_value", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": {"$import" : "array.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": { + "$import" : { + "filename" : "array.json", + "patch" : [ { "element_1" : "patched_value" } ] + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "patched_value" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": {"$import" : "nested_import.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportPatchTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": { + "$import" : { + "filename" : "nested_import.json", + "patch" : { "obj" : { "field_3" : "patched_value" } } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "patched_value" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportCycleTest) + { + const char* inputFile = R"( + { + "name" : "nested_import_cycle", + "object": {"$import" : "nested_import_c1.json"} + } + )"; + + TestImportCycle(inputFile); + } + + TEST_F(JsonImportingTests, InsertNewImportTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "field_1" : "other_value", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_1" : "other_value" } + } + } + } + )"; + + TestInsertNewImport(inputFile, expectedOutput); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index cc6000209f..d39595c45e 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -121,6 +121,7 @@ set(FILES Serialization/Json/TestCases_Classes.cpp Serialization/Json/TestCases_Compare.cpp Serialization/Json/TestCases_Enum.cpp + Serialization/Json/TestCases_Importing.cpp Serialization/Json/TestCases_Patching.cpp Serialization/Json/TestCases_Pointers.h Serialization/Json/TestCases_Pointers.cpp diff --git a/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h b/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h index 22b65f8340..c657e0edc7 100644 --- a/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Matchmaking/IMatchmakingRequests.h @@ -43,7 +43,7 @@ namespace AzFramework class IMatchmakingAsyncRequests { public: - AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}"); + AZ_RTTI(IMatchmakingAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}"); IMatchmakingAsyncRequests() = default; virtual ~IMatchmakingAsyncRequests() = default; @@ -60,4 +60,31 @@ namespace AzFramework // @param stopMatchmakingRequest The request of StopMatchmaking operation virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0; }; + + //! MatchmakingAsyncRequestNotifications + //! The notifications correspond to matchmaking async requests + class MatchmakingAsyncRequestNotifications + : public AZ::EBusTraits + { + public: + // Safeguard handler for multi-threaded use case + using MutexType = AZStd::recursive_mutex; + + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + // OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes + virtual void OnAcceptMatchAsyncComplete() = 0; + + // OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes + // @param matchmakingTicketId The unique identifier for the matchmaking ticket + virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0; + + // OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes + virtual void OnStopMatchmakingAsyncComplete() = 0; + }; + using MatchmakingAsyncRequestNotificationBus = AZ::EBus; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h b/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h index ad61971a11..aa19b94b4a 100644 --- a/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h +++ b/Code/Framework/AzFramework/AzFramework/Matchmaking/MatchmakingNotifications.h @@ -13,36 +13,10 @@ namespace AzFramework { - //! MatchmakingAsyncRequestNotifications - //! The notifications correspond to matchmaking async requests - class MatchmakingAsyncRequestNotifications - : public AZ::EBusTraits - { - public: - // Safeguard handler for multi-threaded use case - using MutexType = AZStd::recursive_mutex; - - ////////////////////////////////////////////////////////////////////////// - // EBusTraits overrides - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - ////////////////////////////////////////////////////////////////////////// - - // OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes - virtual void OnAcceptMatchAsyncComplete() = 0; - - // OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes - // @param matchmakingTicketId The unique identifier for the matchmaking ticket - virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0; - - // OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes - virtual void OnStopMatchmakingAsyncComplete() = 0; - }; - using MatchmakingAsyncRequestNotificationBus = AZ::EBus; - //! MatchmakingNotifications //! The matchmaking notifications to listen for performing required operations - class MatchAcceptanceNotifications + //! based on matchmaking ticket event + class MatchmakingNotifications : public AZ::EBusTraits { public: @@ -55,8 +29,18 @@ namespace AzFramework static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; ////////////////////////////////////////////////////////////////////////// - // OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE + // OnMatchAcceptance is fired when match is found and pending on acceptance + // Use this notification to accept found match virtual void OnMatchAcceptance() = 0; + + // OnMatchComplete is fired when match is complete + virtual void OnMatchComplete() = 0; + + // OnMatchError is fired when match is processed with error + virtual void OnMatchError() = 0; + + // OnMatchFailure is fired when match is failed to complete + virtual void OnMatchFailure() = 0; }; - using MatchAcceptanceNotificationBus = AZ::EBus; + using MatchmakingNotificationBus = AZ::EBus; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp b/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp index 3ba7ed7223..798b1fe99f 100644 --- a/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp +++ b/Code/Framework/AzFramework/AzFramework/Process/ProcessWatcher.cpp @@ -22,7 +22,7 @@ namespace AzFramework AZStd::scoped_ptr pWatcher(LaunchProcess(processLaunchInfo, communicationType)); if (!pWatcher) { - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); + AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); return false; } else @@ -31,7 +31,7 @@ namespace AzFramework ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator(); if (!pCommunicator || !pCommunicator->IsValid()) { - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); + AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str()); return false; } else diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp index af5b3af3d8..79a6333612 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp @@ -308,7 +308,7 @@ namespace AzFramework event.data.data32[2] = 0; event.data.data32[3] = 1; event.data.data32[4] = 0; - xcb_void_cookie_t xcbCheckResult = xcb_send_event( + [[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event( m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (const char*)&event); AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN"); @@ -333,7 +333,7 @@ namespace AzFramework event.data.data32[2] = 0; event.data.data32[3] = 0; event.data.data32[4] = 0; - xcb_void_cookie_t xcbCheckResult = xcb_send_event( + [[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event( m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (const char*)&event); AZ_Assert( diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp index 158240210e..85669e093c 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp @@ -7,17 +7,35 @@ */ #include +#include #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include #endif +constexpr rlim_t g_minimumOpenFileHandles = 65536L; + //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AzFramework { //////////////////////////////////////////////////////////////////////////////////////////////// Application::Implementation* Application::Implementation::Create() { + // The default open file limit for processes may not be enough for O3DE applications. + // We will need to increase to the recommended value if the current open file limit + // is not sufficient. + rlimit currentLimit; + int get_limit_result = getrlimit(RLIMIT_NOFILE, ¤tLimit); + AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits"); + if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles)) + { + rlimit newLimit; + newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit + newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit + [[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit); + AZ_Assert(set_limit_result == 0, "Unable to update open file limits"); + } + #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB return aznew XcbApplication(); #elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp index d2cd681012..51a8545443 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Process/ProcessWatcher_Linux.cpp @@ -91,31 +91,42 @@ namespace AzFramework return processId == 0; } - /*! Executes a command in the child process after the fork operation has been executed. - * This function will never return. If the execvp command fails this will call _exit with - * the errno value as the return value since continuing execution after a execvp command - * is invalid (it will be running the parent's code and in its address space and will - * cause many issues). + /*! Executes a command in the child process after the fork operation + * has been executed. This function will never return. If the execvpe + * command fails this will call _exit since continuing execution after + * a execvpe command is invalid (it will be running the parent's code + * and in its address space and will cause many issues). + * + * This function runs after a `fork()` call. `fork()` creates a copy of + * the current process, including the current state of the process's + * memory, at the time the call is made. However, it only creates a + * copy of the one thread that called `fork()`. This means that if any + * mutexes are locked by other threads at the time of `fork()`, those + * mutexes will remain locked in the child process, with no way to + * unlock them. So this function needs to ensure that it does as little + * work as possible. * * \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer. - * \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer. + * \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer. * \param processLaunchInfo - struct containing information about luanching the command * \param startupInfo - struct containing information needed to startup the command + * \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process */ - void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo) + [[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array& errorPipe) { + close(errorPipe[0]); + if (!processLaunchInfo.m_workingDirectory.empty()) { int res = chdir(processLaunchInfo.m_workingDirectory.c_str()); if (res != 0) { - std::cerr << strerror(errno) << std::endl; - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str()); + write(errorPipe[1], &errno, sizeof(int)); // We *have* to _exit as we are the child process and simply // returning at this point would mean we would start running // the code from our parent process and that will just wreck // havoc. - _exit(errno); + _exit(0); } } @@ -135,15 +146,17 @@ namespace AzFramework startupInfo.SetupHandlesForChildProcess(); - execve(commandAndArgs[0], commandAndArgs, environmentVariables); + execvpe(commandAndArgs[0], commandAndArgs, environmentVariables); + const int errval = errno; - // If we get here then execve failed to run the requested program and + // If we get here then execvpe failed to run the requested program and // we have an error. In this case we need to exit the child process - // to stop it from continuing to run as a clone of the parent - AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno)); - std::cerr << strerror(errno) << std::endl; + // to stop it from continuing to run as a clone of the parent. + // Communicate the error code back to the parent via a pipe for the + // parent to read. + write(errorPipe[1], &errval, sizeof(errval)); - _exit(errno); + _exit(0); } } @@ -212,9 +225,8 @@ namespace AzFramework AZStd::string outputString; bool inQuotes = false; - for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos) + for (const char currentChar : processLaunchInfo.m_commandlineParameters) { - char currentChar = processLaunchInfo.m_commandlineParameters[pos]; if (currentChar == '"') { inQuotes = !inQuotes; @@ -231,7 +243,7 @@ namespace AzFramework outputString.push_back(currentChar); } } - + if (!outputString.empty()) { commandTokens.push_back(outputString); @@ -249,10 +261,10 @@ namespace AzFramework return false; } - // Because of the way execve is defined we need to copy the strings from + // Because of the way execvpe is defined we need to copy the strings from // AZ::string (using c_str() returns a const char*) into a non-const char* - // Need to add one more as exec requires the array's last element to be a null pointer + // Need to add one more as execvpe requires the array's last element to be a null pointer char** commandAndArgs = new char*[commandTokens.size() + 1]; for (int i = 0; i < commandTokens.size(); ++i) { @@ -275,7 +287,7 @@ namespace AzFramework azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str()); environmentVariablesVector.emplace_back(environmentVariable.get()); } - // Adding one more as exec expects the array to have a nullptr as the last element + // Adding one more as execvpe expects the array to have a nullptr as the last element environmentVariablesVector.emplace_back(nullptr); environmentVariables = environmentVariablesVector.data(); } @@ -288,15 +300,50 @@ namespace AzFramework AZ_Assert(environmentVariables, "Environment variables for current process not available\n"); } + // Set up a pipe to communicate the error code from the subprocess's execvpe call + AZStd::array childErrorPipeFds{}; + pipe(childErrorPipeFds.data()); + + // This configures the write end of the pipe to close on calls to `exec` + fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC); + pid_t child_pid = fork(); if (IsIdChildProcess(child_pid)) { - ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo); + ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds); } - processData.m_childProcessId = child_pid; // Close these handles as they are only to be used by the child process processData.m_startupInfo.CloseAllHandles(); + close(childErrorPipeFds[1]); + + { + int errorCodeFromChild = 0; + int count = 0; + // Read from the error pipe. + // * If the child's call to execvpe succeeded, then the pipe will + // be closed due to setting FD_CLOEXEC on the write end of the + // pipe. `read()` will return 0. + // * If the child's call to execvpe failed, the child will have + // written the error code to the pipe. `read()` will return >0, and + // the data to be read is the error code from execvpe. + while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1) + { + if (errno != EAGAIN && errno != EINTR) + { + break; + } + } + if (count) + { + AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild)); + processData.m_childProcessIsDone = true; + child_pid = -1; + } + } + close(childErrorPipeFds[0]); + + processData.m_childProcessId = child_pid; for (int i = 0; i < commandTokens.size(); i++) { diff --git a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp index 864bc3f52a..7581986950 100644 --- a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp +++ b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp @@ -7,6 +7,7 @@ */ #include #include +#include #include #include @@ -20,11 +21,16 @@ public: explicit ModuleHandle(const std::string& lib) : m_libHandle(nullptr) { - std::string libext = lib; - if (!AZ::Test::EndsWith(libext, ".dylib")) + AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() }; + if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX)) { - libext += ".dylib"; + libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native(); } + if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION) + { + libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION; + } + m_libHandle = dlopen(libext.c_str(), RTLD_NOW); const char* error = dlerror(); if (error) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 7db507e751..67b5d99011 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -53,7 +53,7 @@ namespace AzToolsFramework AZ_Assert(m_loaderInterface != nullptr, "Couldn't get prefab loader interface, it's a requirement for PrefabEntityOwnership system to work"); - m_rootInstance = AZStd::unique_ptr(m_prefabSystemComponent->CreatePrefab({}, {}, "NewLevel.prefab")); + m_rootInstance = AZStd::unique_ptr(m_prefabSystemComponent->CreatePrefab({}, {}, "newLevel.prefab")); m_sliceOwnershipService.BusConnect(m_entityContextId); m_sliceOwnershipService.m_shouldAssertForLegacySlicesUsage = m_shouldAssertForLegacySlicesUsage; m_editorSliceOwnershipService.BusConnect(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp index d95a704e84..e9a5bee87a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,13 @@ namespace AzToolsFramework AZ::Edit::SliceFlags::DontGatherReference); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->ConstantProperty( + "EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId))) + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation); + } } void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h index 8873787bd3..aa15b63ac2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h @@ -16,7 +16,9 @@ namespace AzToolsFramework class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase); + static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}"; + + AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp index a79b9eb73d..09b5745a90 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp @@ -28,7 +28,9 @@ namespace AzToolsFramework::Prefab "Instance Entity Mapper Interface could not be found. " "Check that it is being correctly initialized."); + EditorEntityInfoNotificationBus::Handler::BusConnect(); EditorEntityContextNotificationBus::Handler::BusConnect(); + PrefabPublicNotificationBus::Handler::BusConnect(); AZ::Interface::Register(this); AZ::Interface::Register(this); } @@ -37,10 +39,12 @@ namespace AzToolsFramework::Prefab { AZ::Interface::Unregister(this); AZ::Interface::Unregister(this); + PrefabPublicNotificationBus::Handler::BusDisconnect(); EditorEntityContextNotificationBus::Handler::BusDisconnect(); + EditorEntityInfoNotificationBus::Handler::BusDisconnect(); } - void PrefabFocusHandler::Initialize() + void PrefabFocusHandler::InitializeEditorInterfaces() { m_containerEntityInterface = AZ::Interface::Get(); AZ_Assert( @@ -55,13 +59,6 @@ namespace AzToolsFramework::Prefab "Prefab - PrefabFocusHandler - " "Focus Mode Interface could not be found. " "Check that it is being correctly initialized."); - - m_instanceEntityMapperInterface = AZ::Interface::Get(); - AZ_Assert( - m_instanceEntityMapperInterface, - "Prefab - PrefabFocusHandler - " - "Instance Entity Mapper Interface could not be found. " - "Check that it is being correctly initialized."); } PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId) @@ -90,12 +87,12 @@ namespace AzToolsFramework::Prefab PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index) { - if (index < 0 || index >= m_instanceFocusVector.size()) + if (index < 0 || index >= m_instanceFocusHierarchy.size()) { return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex.")); } - InstanceOptionalReference focusedInstance = m_instanceFocusVector[index]; + InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index]; FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId()); @@ -134,42 +131,38 @@ namespace AzToolsFramework::Prefab return AZ::Failure(AZStd::string("Prefab Focus Handler: invalid instance to focus on.")); } - if (!m_isInitialized) + // Close all container entities in the old path. + CloseInstanceContainers(m_instanceFocusHierarchy); + + m_focusedInstance = focusedInstance; + m_focusedTemplateId = focusedInstance->get().GetTemplateId(); + + AZ::EntityId containerEntityId; + + if (focusedInstance->get().GetParentInstance() != AZStd::nullopt) { - Initialize(); + containerEntityId = focusedInstance->get().GetContainerEntityId(); } - - if (!m_focusedInstance.has_value() || &m_focusedInstance->get() != &focusedInstance->get()) + else { - // Close all container entities in the old path - CloseInstanceContainers(m_instanceFocusVector); - - m_focusedInstance = focusedInstance; - m_focusedTemplateId = focusedInstance->get().GetTemplateId(); - - AZ::EntityId containerEntityId; - - if (focusedInstance->get().GetParentInstance() != AZStd::nullopt) - { - containerEntityId = focusedInstance->get().GetContainerEntityId(); - } - else - { - containerEntityId = AZ::EntityId(); - } + containerEntityId = AZ::EntityId(); + } - // Focus on the descendants of the container entity + // Focus on the descendants of the container entity in the Editor, if the interface is initialized. + if (m_focusModeInterface) + { m_focusModeInterface->SetFocusRoot(containerEntityId); - - // Refresh path variables - RefreshInstanceFocusList(); - - // Open all container entities in the new path - OpenInstanceContainers(m_instanceFocusVector); - - PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); } + // Refresh path variables. + RefreshInstanceFocusList(); + RefreshInstanceFocusPath(); + + // Open all container entities in the new path. + OpenInstanceContainers(m_instanceFocusHierarchy); + + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + return AZ::Success(); } @@ -220,49 +213,80 @@ namespace AzToolsFramework::Prefab const int PrefabFocusHandler::GetPrefabFocusPathLength([[maybe_unused]] AzFramework::EntityContextId entityContextId) const { - return aznumeric_cast(m_instanceFocusVector.size()); + return aznumeric_cast(m_instanceFocusHierarchy.size()); } - void PrefabFocusHandler::OnEntityStreamLoadSuccess() + void PrefabFocusHandler::OnContextReset() { - if (!m_isInitialized) - { - Initialize(); - } - // Clear the old focus vector - m_instanceFocusVector.clear(); + m_instanceFocusHierarchy.clear(); // Focus on the root prefab (AZ::EntityId() will default to it) FocusOnPrefabInstanceOwningEntityId(AZ::EntityId()); } + void PrefabFocusHandler::OnEntityInfoUpdatedName(AZ::EntityId entityId, [[maybe_unused]]const AZStd::string& name) + { + // Determine if the entityId is the container for any of the instances in the vector + auto result = AZStd::find_if( + m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(), + [entityId](const InstanceOptionalReference& instance) + { + return (instance->get().GetContainerEntityId() == entityId); + } + ); + + if (result != m_instanceFocusHierarchy.end()) + { + // Refresh the path and notify changes. + RefreshInstanceFocusPath(); + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + } + } + + void PrefabFocusHandler::OnPrefabInstancePropagationEnd() + { + // Refresh the path and notify changes in case propagation updated any container names. + RefreshInstanceFocusPath(); + PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged); + } + void PrefabFocusHandler::RefreshInstanceFocusList() { - m_instanceFocusVector.clear(); - m_instanceFocusPath.clear(); + m_instanceFocusHierarchy.clear(); AZStd::list instanceFocusList; - // Use a support list to easily push front while traversing the prefab hierarchy InstanceOptionalReference currentInstance = m_focusedInstance; while (currentInstance.has_value()) { - instanceFocusList.push_front(currentInstance); + m_instanceFocusHierarchy.emplace_back(currentInstance); currentInstance = currentInstance->get().GetParentInstance(); } - // Populate internals using the support list - for (auto& instance : instanceFocusList) + // Invert the vector, since we need the top instance to be at index 0 + AZStd::reverse(m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end()); + } + + void PrefabFocusHandler::RefreshInstanceFocusPath() + { + m_instanceFocusPath.clear(); + + for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy) { m_instanceFocusPath.Append(instance->get().GetContainerEntity()->get().GetName()); - m_instanceFocusVector.emplace_back(instance); } } void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector& instances) const { + // If this is called outside the Editor, this interface won't be initialized. + if (!m_containerEntityInterface) + { + return; + } + for (const InstanceOptionalReference& instance : instances) { if (instance.has_value()) @@ -274,6 +298,12 @@ namespace AzToolsFramework::Prefab void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector& instances) const { + // If this is called outside the Editor, this interface won't be initialized. + if (!m_containerEntityInterface) + { + return; + } + for (const InstanceOptionalReference& instance : instances) { if (instance.has_value()) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h index 80b7a6859c..6a71365d8e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h @@ -11,9 +11,11 @@ #include #include +#include #include #include #include +#include #include namespace AzToolsFramework @@ -30,7 +32,9 @@ namespace AzToolsFramework::Prefab class PrefabFocusHandler final : private PrefabFocusInterface , private PrefabFocusPublicInterface + , private PrefabPublicNotificationBus::Handler , private EditorEntityContextNotificationBus::Handler + , private EditorEntityInfoNotificationBus::Handler { public: AZ_CLASS_ALLOCATOR(PrefabFocusHandler, AZ::SystemAllocator, 0); @@ -38,9 +42,8 @@ namespace AzToolsFramework::Prefab PrefabFocusHandler(); ~PrefabFocusHandler(); - void Initialize(); - // PrefabFocusInterface overrides ... + void InitializeEditorInterfaces() override; PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override; TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override; InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override; @@ -54,25 +57,34 @@ namespace AzToolsFramework::Prefab const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override; // EditorEntityContextNotificationBus overrides ... - void OnEntityStreamLoadSuccess() override; + void OnContextReset() override; + + // EditorEntityInfoNotificationBus overrides ... + void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override; + + // PrefabPublicNotifications overrides ... + void OnPrefabInstancePropagationEnd(); private: PrefabFocusOperationResult FocusOnPrefabInstance(InstanceOptionalReference focusedInstance); void RefreshInstanceFocusList(); + void RefreshInstanceFocusPath(); void OpenInstanceContainers(const AZStd::vector& instances) const; void CloseInstanceContainers(const AZStd::vector& instances) const; + //! The instance the editor is currently focusing on. InstanceOptionalReference m_focusedInstance; + //! The templateId of the focused instance. TemplateId m_focusedTemplateId; - AZStd::vector m_instanceFocusVector; + //! The list of instances going from the root (index 0) to the focused instance. + AZStd::vector m_instanceFocusHierarchy; + //! A path containing the names of the containers in the instance focus hierarchy, separated with a /. AZ::IO::Path m_instanceFocusPath; ContainerEntityInterface* m_containerEntityInterface = nullptr; FocusModeInterface* m_focusModeInterface = nullptr; InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr; - - bool m_isInitialized = false; }; } // namespace AzToolsFramework::Prefab diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h index 25c83b89bc..287cbbaf96 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusInterface.h @@ -26,6 +26,11 @@ namespace AzToolsFramework::Prefab public: AZ_RTTI(PrefabFocusInterface, "{F3CFA37B-5FD8-436A-9C30-60EB54E350E1}"); + //! Initializes the editor interfaces for Prefab Focus mode. + //! If this is not called on initialization, the Prefab Focus Mode functions will still work + //! but won't trigger the Editor APIs to visualize focus mode on the UI. + virtual void InitializeEditorInterfaces() = 0; + //! Set the focused prefab instance to the owning instance of the entityId provided. //! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on. virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index ee34b628bb..d976c91c3e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -974,7 +974,7 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } - PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) { if (entityIds.empty()) { @@ -1021,6 +1021,7 @@ namespace AzToolsFramework ScopedUndoBatch undoBatch("Duplicate Entities"); + EntityIdList duplicatedEntityAndInstanceIds; { AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); @@ -1033,7 +1034,7 @@ namespace AzToolsFramework if (!retrieveEntitiesAndInstancesOutcome.IsSuccess()) { - return AZStd::move(retrieveEntitiesAndInstancesOutcome); + return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError()); } // Take a snapshot of the instance DOM before we manipulate it @@ -1044,8 +1045,6 @@ namespace AzToolsFramework PrefabDom instanceDomAfter; instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); - EntityIdList duplicatedEntityAndInstanceIds; - // Duplicate any nested entities and instances as requested AZStd::unordered_map newInstanceAliasToOldInstanceMap; AZStd::unordered_map duplicateEntityAliasMap; @@ -1114,7 +1113,7 @@ namespace AzToolsFramework ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); } - return AZ::Success(); + return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds)); } PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index a9dadc3336..4961be9d77 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -63,7 +63,7 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; - PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 528d4f6d1b..ede857dd2b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -26,6 +26,7 @@ namespace AzToolsFramework { typedef AZ::Outcome CreatePrefabResult; typedef AZ::Outcome InstantiatePrefabResult; + typedef AZ::Outcome DuplicatePrefabResult; typedef AZ::Outcome PrefabOperationResult; typedef AZ::Outcome PrefabRequestResult; typedef AZ::Outcome PrefabEntityResult; @@ -160,14 +161,15 @@ namespace AzToolsFramework /** * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. * @param entities The entities to duplicate. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; /** * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting * the container entity into a regular entity and putting it under the parent prefab, removing the link between this - * instance and the parent, removing links between this instance and it's nested instances, adding entities directly + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly * owned by this instance under the parent instance. * Bails if the entity is not a container entity or belongs to the level prefab instance. * @param containerEntityId The container entity id of the instance to detach. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h index 7b23fffb7f..fd4b8a5f17 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h @@ -25,6 +25,7 @@ namespace AzToolsFramework { using CreatePrefabResult = AZ::Outcome; using InstantiatePrefabResult = AZ::Outcome; + using DuplicatePrefabResult = AZ::Outcome; using PrefabOperationResult = AZ::Outcome; /** @@ -69,6 +70,29 @@ namespace AzToolsFramework * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting + * the container entity into a regular entity and putting it under the parent prefab, removing the link between this + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly + * owned by this instance under the parent instance. + * Bails if the entity is not a container entity or belongs to the level prefab instance. + * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. + */ + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + + /** + * Get the file path to the prefab file for the prefab instance owning the entity provided. + * Returns the path to the prefab, or an empty path if the entity is owned by the level. + */ + virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0; }; using PrefabPublicRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp index 0aaf81c4c9..3b69dcdfe4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp @@ -28,6 +28,9 @@ namespace AzToolsFramework ->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory) ->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab) ->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance) + ->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab) + ->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance) + ->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath) ; } } @@ -62,5 +65,19 @@ namespace AzToolsFramework return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds); } + PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId) + { + return m_prefabPublicInterface->DetachPrefab(containerEntityId); + } + + DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds); + } + + AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const + { + return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native(); + } } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h index ae0ed2a5d1..b24ea7ec2a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h @@ -34,6 +34,9 @@ namespace AzToolsFramework CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override; private: PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp index 6c96209d56..1c2230fa83 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp @@ -23,7 +23,7 @@ namespace AzToolsFramework } //PrefabInstanceUndo - PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation) + PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation) : PrefabUndoBase(undoOperationName) { m_useImmediatePropagation = useImmediatePropagation; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h index 0946a36951..bc0b86a8c6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h @@ -45,7 +45,7 @@ namespace AzToolsFramework : public PrefabUndoBase { public: - explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true); + explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true); void Capture( const PrefabDom& initialState, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp index 3bd12b1ad3..acb2cd4b1c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp @@ -20,6 +20,8 @@ #include #include +#include + namespace AzToolsFramework { namespace @@ -75,9 +77,17 @@ namespace AzToolsFramework m_resolveKey = true; m_testTrust = false; + // set up signals before we start thread. m_shutdownThreadSignal = false; - m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this)); + + // Check to see if the 'p4' command is available at the command line + int p4VersionExitCode = QProcess::execute("p4", QStringList{ "-V" }); + m_p4ApplicationDetected = (p4VersionExitCode == 0); + if (m_p4ApplicationDetected) + { + m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this)); + } SourceControlConnectionRequestBus::Handler::BusConnect(); SourceControlCommandBus::Handler::BusConnect(); @@ -88,10 +98,13 @@ namespace AzToolsFramework SourceControlCommandBus::Handler::BusDisconnect(); SourceControlConnectionRequestBus::Handler::BusDisconnect(); - m_shutdownThreadSignal = true; // tell the thread to die. - m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal - m_WorkerThread.join(); // wait for the thread to finish. - m_WorkerThread = AZStd::thread(); + if (m_p4ApplicationDetected) + { + m_shutdownThreadSignal = true; // tell the thread to die. + m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal + m_WorkerThread.join(); // wait for the thread to finish. + m_WorkerThread = AZStd::thread(); + } SetConnection(nullptr); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h index 858d9c0103..e5ccf7ad29 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h @@ -260,5 +260,7 @@ namespace AzToolsFramework AZStd::atomic_bool m_validConnection; SourceControlState m_connectionState; + + bool m_p4ApplicationDetected { false }; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 526912cf29..65f7b0cdfb 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -135,6 +136,10 @@ namespace AzToolsFramework return; } + // Initialize Editor functionality for the Prefab Focus Handler + auto prefabFocusInterface = AZ::Interface::Get(); + prefabFocusInterface->InitializeEditorInterfaces(); + EditorContextMenuBus::Handler::BusConnect(); EditorEventsBus::Handler::BusConnect(); PrefabInstanceContainerNotificationBus::Handler::BusConnect(); @@ -250,7 +255,7 @@ namespace AzToolsFramework if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity)) { // Edit Prefab - if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity)) + if (!s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity)) { QAction* editAction = menu->addAction(QObject::tr("Edit Prefab")); editAction->setToolTip(QObject::tr("Edit the prefab in focus mode.")); @@ -1154,25 +1159,14 @@ namespace AzToolsFramework { s_editorEntityUiInterface->RegisterEntity(entityId, m_prefabUiHandler.GetHandlerId()); - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); - - if (prefabWipFeaturesEnabled) - { - // Register entity as a container - s_containerEntityInterface->RegisterEntityAsContainer(entityId); - } + // Register entity as a container + s_containerEntityInterface->RegisterEntityAsContainer(entityId); } } void PrefabIntegrationManager::OnPrefabComponentDeactivate(AZ::EntityId entityId) { - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); - - if (prefabWipFeaturesEnabled && !s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId)) + if (!s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId)) { // Unregister entity as a container s_containerEntityInterface->UnregisterEntityAsContainer(entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp index ccad85e32b..7c2c65d204 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp @@ -316,14 +316,7 @@ namespace AzToolsFramework void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const { - bool prefabWipFeaturesEnabled = false; - AzFramework::ApplicationRequests::Bus::BroadcastResult( - prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled); - - if (prefabWipFeaturesEnabled) - { - // Focus on this prefab - m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); - } + // Focus on this prefab + m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); } } diff --git a/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp b/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp index ea458ba826..2d3e671999 100644 --- a/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp +++ b/Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp @@ -72,7 +72,7 @@ struct FolderRootWatch::PlatformImplementation // 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_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout)) { @@ -95,7 +95,7 @@ struct FolderRootWatch::PlatformImplementation int watchHandle = inotify_add_watch(m_iNotifyHandle, dirName.toUtf8().constData(), - IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY); + IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE); if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout)) { diff --git a/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp b/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp index 63536a160b..516f6beb3c 100644 --- a/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp +++ b/Code/Tools/AssetProcessor/native/tests/AssetProcessorMessagesTests.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -98,10 +100,26 @@ namespace AssetProcessorMessagesTests int argC = 0; m_batchApplicationManager = AZStd::make_unique(&argC, nullptr, nullptr); - m_batchApplicationManager->BeforeRun(); - // Override Game Name to be "AutomatedTesting" - AssetUtilities::ComputeProjectName("AutomatedTesting", true); + auto registry = AZ::SettingsRegistry::Get(); + EXPECT_NE(registry, nullptr); + constexpr AZ::SettingsRegistryInterface::FixedValueString bootstrapKey{ + AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey + }; + constexpr AZ::SettingsRegistryInterface::FixedValueString projectPathKey{ bootstrapKey + "/project_path" }; + registry->Set(projectPathKey, "AutomatedTesting"); + AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry); + + // Force the branch token into settings registry before starting the application manager. + // This avoids writing the asset_processor.setreg file which can cause fileIO errors. + const AZ::IO::FixedMaxPathString enginePath = AZ::Utils::GetEnginePath(); + constexpr AZ::SettingsRegistryInterface::FixedValueString branchTokenKey{ bootstrapKey + "/assetProcessor_branch_token" }; + AZStd::string token; + AZ::StringFunc::AssetPath::CalculateBranchToken(enginePath.c_str(), token); + registry->Set(branchTokenKey, token.c_str()); + + auto status = m_batchApplicationManager->BeforeRun(); + ASSERT_EQ(status, ApplicationManager::BeforeRunStatus::Status_Success); m_batchApplicationManager->m_platformConfiguration = new PlatformConfiguration(); m_batchApplicationManager->InitAssetProcessorManager(); @@ -159,21 +177,25 @@ namespace AssetProcessorMessagesTests ASSERT_TRUE(result); }); - - } void TearDown() override { - QEventLoop eventLoop; + if (m_batchApplicationManager->m_connectionManager) + { + QEventLoop eventLoop; - QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit); + QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit); - m_batchApplicationManager->m_connectionManager->QuitRequested(); + m_batchApplicationManager->m_connectionManager->QuitRequested(); - eventLoop.exec(); + eventLoop.exec(); + } - m_assetSystemComponent->Deactivate(); + if (m_assetSystemComponent) + { + m_assetSystemComponent->Deactivate(); + } m_batchApplicationManager->Destroy(); } diff --git a/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp b/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp index c237f4801e..a024ce6c7a 100644 --- a/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/ApplicationManager.cpp @@ -505,6 +505,14 @@ bool ApplicationManager::StartAZFramework() AzFramework::Application::Descriptor appDescriptor; AZ::ComponentApplication::StartupParameters params; + QDir projectPath{ AssetUtilities::ComputeProjectPath() }; + if (!projectPath.exists("project.json")) + { + AZStd::string errorMsg = AZStd::string::format("Path '%s' is not a valid project path.", projectPath.path().toUtf8().constData()); + AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::OnErrorMessage, errorMsg.c_str()); + return false; + } + QString projectName = AssetUtilities::ComputeProjectName(); // Prevent loading of gems in the Create method of the ComponentApplication @@ -520,7 +528,6 @@ bool ApplicationManager::StartAZFramework() //Registering all the Components m_frameworkApp.RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor()); - Reflect(); const AzFramework::CommandLine* commandLine = nullptr; diff --git a/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp b/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp index 40d3bd3caa..c3ff22a39e 100644 --- a/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/GUIApplicationManager.cpp @@ -95,6 +95,8 @@ GUIApplicationManager::~GUIApplicationManager() ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun() { + AssetProcessor::MessageInfoBus::Handler::BusConnect(); + ApplicationManager::BeforeRunStatus status = ApplicationManagerBase::BeforeRun(); if (status != ApplicationManager::BeforeRunStatus::Status_Success) { @@ -109,7 +111,6 @@ ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun() #if defined(EXTERNAL_CRASH_REPORTING) CrashHandler::ToolsCrashHandler::InitCrashHandler("AssetProcessor", projectAssetRoot.absolutePath().toStdString()); #endif - AssetProcessor::MessageInfoBus::Handler::BusConnect(); // we have to monitor both the cache folder and the database file and restart AP if either of them gets deleted // It is important to note that we are monitoring the parent folder and not the actual cache folder itself since @@ -436,98 +437,7 @@ bool GUIApplicationManager::OnError(const char* /*window*/, const char* message) connection = Qt::QueuedConnection; } - if (m_isCurrentlyLoadingGems) - { - // if something goes wrong during gem initialization, this is a special case and we need to be extra helpful. - const char* userSettingsFile = "_WAF_/user_settings.options"; - const char* defaultSettingsFile = "_WAF_/default_settings.json"; - - QDir engineRoot; - AssetUtilities::ComputeEngineRoot(engineRoot); - - QString settingsPath = engineRoot.absoluteFilePath(userSettingsFile); - QString friendlyErrorMessage; - bool usingDefaults = false; - - if (QFile::exists(settingsPath)) - { - QSettings loader(settingsPath, QSettings::IniFormat); - QVariant settingValue = loader.value("Game Projects/enabled_game_projects"); - QStringList compiledProjects = settingValue.toStringList(); - - if (compiledProjects.isEmpty()) - { - QByteArray byteArray; - QFile jsonFile; - jsonFile.setFileName(engineRoot.absoluteFilePath(defaultSettingsFile)); - jsonFile.open(QIODevice::ReadOnly | QIODevice::Text); - byteArray = jsonFile.readAll(); - jsonFile.close(); - - QJsonObject settingsObject = QJsonDocument::fromJson(byteArray).object(); - QJsonArray projectsArray = settingsObject["Game Projects"].toArray(); - - if (!projectsArray.isEmpty()) - { - auto projectObject = projectsArray[0].toObject(); - QString projects = projectObject["default_value"].toString(); - - if (!projects.isEmpty()) - { - compiledProjects = projects.split(','); - usingDefaults = true; - } - } - } - - for (int i = 0; i < compiledProjects.size(); ++i) - { - compiledProjects[i] = compiledProjects[i].trimmed(); - } - - QString enabledProject = AssetUtilities::ComputeProjectName(); - - if (!compiledProjects.contains(enabledProject)) - { - QString projectSourceLine; - - if (usingDefaults) - { - projectSourceLine = QString("The currently compiled projects according to the defaults in %1 are '%2'").arg(defaultSettingsFile); - } - else - { - projectSourceLine = QString("The currently compiled projects according to %1 are '%2'").arg(userSettingsFile); - } - - projectSourceLine = projectSourceLine.arg(compiledProjects.join(", ")); - friendlyErrorMessage = QString("An error occurred while loading gems.\n" - "The enabled game project is not in the list of compiled projects.\n" - "Please configure the enabled project to be compiled and rebuild or change the enabled project.\n" - "The currently enabled game project (from bootstrap.cfg or /%4 command-line parameter) is '%1'.\n" - "%2\n" - "Full error text:\n" - "%3" - ).arg(enabledProject).arg(projectSourceLine).arg(message).arg(AssetUtilities::ProjectPathOverrideParameter); - } - } - - if (friendlyErrorMessage.isEmpty()) - { - friendlyErrorMessage = QString("An error occurred while loading gems.\n" - "This can happen when new gems are added to a project, but those gems need to be built in order to function.\n" - "This can also happen when switching to a different project, one which uses gems which are not yet built.\n" - "To continue, please build the current project before attempting to run Asset Processor again.\n\n" - "Full error text:\n" - "%1").arg(message); - } - QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, friendlyErrorMessage), Q_ARG(bool, true)); - } - else - { - QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true)); - } - + QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true)); return true; } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h similarity index 53% rename from Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h rename to Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h index c14ef559b2..8ab215d741 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/IAWSGameLiftRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftMatchmakingRequestBus.h @@ -5,75 +5,16 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - + #pragma once #include -#include #include #include -#include namespace AWSGameLift { - //! IAWSGameLiftRequests - //! GameLift Gem interfaces to configure client manager - class IAWSGameLiftRequests - { - public: - AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}"); - - IAWSGameLiftRequests() = default; - virtual ~IAWSGameLiftRequests() = default; - - //! ConfigureGameLiftClient - //! Configure GameLift client to interact with Amazon GameLift service - //! @param region Specifies the AWS region to use - //! @return True if client configuration succeeds, false otherwise - virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0; - - //! CreatePlayerId - //! Create a new, random ID number for every player in every new game session. - //! @param includeBrackets Whether includes brackets in player id - //! @param includeDashes Whether includes dashes in player id - //! @return The player id to use in game session - virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0; - }; - - // IAWSGameLiftRequests EBus wrapper for scripting - class AWSGameLiftRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftRequestBus = AZ::EBus; - - // ISessionAsyncRequests EBus wrapper for scripting - class AWSGameLiftSessionAsyncRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftSessionAsyncRequestBus = AZ::EBus; - - // ISessionRequests EBus wrapper for scripting - class AWSGameLiftSessionRequests - : public AZ::EBusTraits - { - public: - using MutexType = AZStd::recursive_mutex; - static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; - static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; - }; - using AWSGameLiftSessionRequestBus = AZ::EBus; - - // IMatchmakingAsyncRequests EBus wrapper for scripting + // IMatchmakingAsyncRequests EBus wrapper class AWSGameLiftMatchmakingAsyncRequests : public AZ::EBusTraits { @@ -84,7 +25,7 @@ namespace AWSGameLift }; using AWSGameLiftMatchmakingAsyncRequestBus = AZ::EBus; - // IMatchmakingRequests EBus wrapper for scripting + // IMatchmakingRequests EBus wrapper class AWSGameLiftMatchmakingRequests : public AZ::EBusTraits { @@ -121,7 +62,7 @@ namespace AWSGameLift virtual void StopPolling() = 0; }; - // IAWSGameLiftMatchmakingEventRequests EBus wrapper for scripting + // IAWSGameLiftMatchmakingEventRequests EBus wrapper class AWSGameLiftMatchmakingEventRequests : public AZ::EBusTraits { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h new file mode 100644 index 0000000000..e1951d1e61 --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftRequestBus.h @@ -0,0 +1,51 @@ +/* + * 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 +#include + +namespace AWSGameLift +{ + //! IAWSGameLiftRequests + //! GameLift Gem interfaces to configure GameLift client and other help functions, + //! like creating random GameLift player id + class IAWSGameLiftRequests + { + public: + AZ_RTTI(IAWSGameLiftRequests, "{494167AD-1185-4AF3-8BF9-C8C37FC9C199}"); + + IAWSGameLiftRequests() = default; + virtual ~IAWSGameLiftRequests() = default; + + //! ConfigureGameLiftClient + //! Configure GameLift client to interact with Amazon GameLift service + //! @param region Specifies the AWS region to use + //! @return True if client configuration succeeds, false otherwise + virtual bool ConfigureGameLiftClient(const AZStd::string& region) = 0; + + //! CreatePlayerId + //! Create a new, random ID number for every player in every new game session. + //! @param includeBrackets Whether includes brackets in player id + //! @param includeDashes Whether includes dashes in player id + //! @return The player id to use in game session + virtual AZStd::string CreatePlayerId(bool includeBrackets, bool includeDashes) = 0; + }; + + // IAWSGameLiftRequests EBus wrapper + class AWSGameLiftRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftRequestBus = AZ::EBus; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h new file mode 100644 index 0000000000..c99509ca3f --- /dev/null +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Include/Request/AWSGameLiftSessionRequestBus.h @@ -0,0 +1,38 @@ +/* + * 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 +#include +#include + +namespace AWSGameLift +{ + // ISessionAsyncRequests EBus wrapper + class AWSGameLiftSessionAsyncRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionAsyncRequestBus = AZ::EBus; + + // ISessionRequests EBus wrapper + class AWSGameLiftSessionRequests + : public AZ::EBusTraits + { + public: + using MutexType = AZStd::recursive_mutex; + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + using AWSGameLiftSessionRequestBus = AZ::EBus; +} // namespace AWSGameLift diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp index cc2f84cb35..2e978402cd 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.cpp @@ -97,6 +97,7 @@ namespace AWSGameLift AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is complete.", ticket.GetTicketId().c_str()); RequestPlayerJoinMatch(ticket, playerId); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchComplete); m_status = TicketTrackerStatus::Idle; return; } @@ -104,25 +105,28 @@ namespace AWSGameLift ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::FAILED || ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::CANCELLED) { - AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s", - ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str()); + AZ_Warning(AWSGameLiftClientLocalTicketTrackerName, false, "Matchmaking ticket %s is not complete, %s", + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchFailure); m_status = TicketTrackerStatus::Idle; return; } else if (ticket.GetStatus() == Aws::GameLift::Model::MatchmakingConfigurationStatus::REQUIRES_ACCEPTANCE) { - // broadcast acceptance requires to player - AzFramework::MatchAcceptanceNotificationBus::Broadcast(&AzFramework::MatchAcceptanceNotifications::OnMatchAcceptance); + AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is pending on acceptance, %s.", + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchAcceptance); } else { AZ_TracePrintf(AWSGameLiftClientLocalTicketTrackerName, "Matchmaking ticket %s is processing, %s.", - ticket.GetTicketId().c_str(), ticket.GetStatusReason().c_str()); + ticket.GetTicketId().c_str(), ticket.GetStatusMessage().c_str()); } } else { AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, "Unable to find expected ticket with id %s", ticketId.c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } } else @@ -130,11 +134,13 @@ namespace AWSGameLift AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftErrorMessageTemplate, describeMatchmakingOutcome.GetError().GetExceptionName().c_str(), describeMatchmakingOutcome.GetError().GetMessage().c_str()); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } } else { AZ_Error(AWSGameLiftClientLocalTicketTrackerName, false, AWSGameLiftClientMissingErrorMessage); + AzFramework::MatchmakingNotificationBus::Broadcast(&AzFramework::MatchmakingNotifications::OnMatchError); } m_waitEvent.try_acquire_for(AZStd::chrono::milliseconds(m_pollingPeriodInMS)); } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h index 04bdd71c85..9fd7f76e1d 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientLocalTicketTracker.h @@ -12,7 +12,7 @@ #include #include -#include +#include #include diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp index 224f16481e..4ee2d31ebf 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -75,7 +76,15 @@ namespace AWSGameLift bool AWSGameLiftClientManager::ConfigureGameLiftClient(const AZStd::string& region) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + Aws::Client::ClientConfiguration clientConfig; + AWSCore::AwsApiJobConfig* defaultConfig = nullptr; + AWSCore::AWSCoreRequestBus::BroadcastResult(defaultConfig, &AWSCore::AWSCoreRequests::GetDefaultConfig); + if (defaultConfig) + { + clientConfig = defaultConfig->GetClientConfiguration(); + } + // Set up client endpoint or region AZStd::string localEndpoint = ""; #if defined(AWSGAMELIFT_DEV) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h index 1f32b69f75..8a0c91c36d 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientManager.h @@ -8,10 +8,13 @@ #pragma once +#include #include #include -#include +#include +#include +#include namespace AWSGameLift { @@ -23,22 +26,37 @@ namespace AWSGameLift struct AWSGameLiftStartMatchmakingRequest; struct AWSGameLiftStopMatchmakingRequest; - // MatchAcceptanceNotificationBus EBus handler for scripting - class AWSGameLiftMatchAcceptanceNotificationBusHandler - : public AzFramework::MatchAcceptanceNotificationBus::Handler + // MatchmakingNotificationBus EBus handler for scripting + class AWSGameLiftMatchmakingNotificationBusHandler + : public AzFramework::MatchmakingNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER( - AWSGameLiftMatchAcceptanceNotificationBusHandler, + AWSGameLiftMatchmakingNotificationBusHandler, "{CBE057D3-F5CE-46D3-B02D-8A6A1446B169}", AZ::SystemAllocator, - OnMatchAcceptance); + OnMatchAcceptance, OnMatchComplete, OnMatchError, OnMatchFailure); void OnMatchAcceptance() override { Call(FN_OnMatchAcceptance); } + + void OnMatchComplete() override + { + Call(FN_OnMatchComplete); + } + + void OnMatchError() override + { + Call(FN_OnMatchError); + } + + void OnMatchFailure() override + { + Call(FN_OnMatchFailure); + } }; // MatchmakingAsyncRequestNotificationBus EBus handler for scripting diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp index 981c1599c9..d256c6c33c 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp @@ -65,13 +65,6 @@ namespace AWSGameLift ->Event("CreatePlayerId", &AWSGameLiftRequestBus::Events::CreatePlayerId, { { { "IncludeBrackets", "" }, { "IncludeDashes", "" } } }); - - behaviorContext->EBus("AWSGameLiftMatchmakingEventRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling, - { { { "TicketId", "" }, - { "PlayerId", "" } } }) - ->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling); } } @@ -128,7 +121,7 @@ namespace AWSGameLift if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { behaviorContext->EBus("AWSGameLiftMatchmakingAsyncRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") ->Event("AcceptMatchAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::AcceptMatchAsync, { { { "AcceptMatchRequest", "" } } }) ->Event("StartMatchmakingAsync", &AWSGameLiftMatchmakingAsyncRequestBus::Events::StartMatchmakingAsync, @@ -137,20 +130,28 @@ namespace AWSGameLift { { { "StopMatchmakingRequest", "" } } }); behaviorContext->EBus("AWSGameLiftMatchmakingAsyncRequestNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") ->Handler(); behaviorContext->EBus("AWSGameLiftMatchmakingRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch, { { { "AcceptMatchRequest", "" } } }) + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Event("AcceptMatch", &AWSGameLiftMatchmakingRequestBus::Events::AcceptMatch, + { { { "AcceptMatchRequest", "" } } }) ->Event("StartMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StartMatchmaking, { { { "StartMatchmakingRequest", "" } } }) ->Event("StopMatchmaking", &AWSGameLiftMatchmakingRequestBus::Events::StopMatchmaking, { { { "StopMatchmakingRequest", "" } } }); - behaviorContext->EBus("AWSGameLiftMatchAcceptanceNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") - ->Handler(); + behaviorContext->EBus("AWSGameLiftMatchmakingEventRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling, + { { { "TicketId", "" }, + { "PlayerId", "" } } }) + ->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling); + + behaviorContext->EBus("AWSGameLiftMatchmakingNotificationBus") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Matchmaking") + ->Handler(); } } @@ -166,7 +167,7 @@ namespace AWSGameLift if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) { behaviorContext->EBus("AWSGameLiftSessionAsyncRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Event("CreateSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::CreateSessionAsync, { { { "CreateSessionRequest", "" } } }) ->Event("JoinSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::JoinSessionAsync, { { { "JoinSessionRequest", "" } } }) @@ -175,11 +176,11 @@ namespace AWSGameLift ->Event("LeaveSessionAsync", &AWSGameLiftSessionAsyncRequestBus::Events::LeaveSessionAsync); behaviorContext->EBus("AWSGameLiftSessionAsyncRequestNotificationBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Handler(); behaviorContext->EBus("AWSGameLiftSessionRequestBus") - ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift") + ->Attribute(AZ::Script::Attributes::Category, "AWSGameLift/Session") ->Event("CreateSession", &AWSGameLiftSessionRequestBus::Events::CreateSession, { { { "CreateSessionRequest", "" } } }) ->Event("JoinSession", &AWSGameLiftSessionRequestBus::Events::JoinSession, { { { "JoinSessionRequest", "" } } }) ->Event("SearchSessions", &AWSGameLiftSessionRequestBus::Events::SearchSessions, { { { "SearchSessionsRequest", "" } } }) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h index dbac9a798a..7e17f085b3 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/Request/IAWSGameLiftInternalRequests.h @@ -20,7 +20,7 @@ namespace Aws namespace AWSGameLift { - //! IAWSGameLiftRequests + //! IAWSGameLiftInternalRequests //! GameLift Gem internal interface which is used to fetch gem global GameLift client class IAWSGameLiftInternalRequests { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp index 4dc4dd85f6..be72de555e 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp @@ -82,27 +82,12 @@ protected: m_gameliftClientMockPtr.reset(); } - void WaitForProcessFinish(uint64_t expectedNum) + void WaitForProcessFinish(AZStd::function processFinishCondition) { int processingTime = 0; while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS) { - if (::UnitTest::TestRunner::Instance().m_numAssertsFailed == expectedNum) - { - AZ_TEST_STOP_TRACE_SUPPRESSION(expectedNum); - return; - } - AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(TEST_WAIT_BUFFER_TIME_MS)); - processingTime += TEST_WAIT_BUFFER_TIME_MS; - } - } - - void WaitForProcessFinish() - { - int processingTime = 0; - while (processingTime < TEST_WAIT_MAXIMUM_TIME_MS) - { - if (m_gameliftClientTicketTracker->IsTrackerIdle()) + if (processFinishCondition()) { return; } @@ -119,19 +104,27 @@ public: TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithoutClientSetup_GetExpectedErrors) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_MultipleCallsWithoutClientSetup_GetExpectedErrors) { AZ::Interface::Get()->SetGameLiftClient(nullptr); + + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -144,9 +137,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButWithFailedOu .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -161,9 +157,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithMoreThanOne .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -189,13 +188,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithCompleteSta .Times(1) .WillOnce(::testing::Return(outcome)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -217,9 +218,12 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButNoPlayerSess .Times(1) .WillOnce(::testing::Return(outcome)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -245,14 +249,17 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButFailedToJoin .Times(1) .WillOnce(::testing::Return(outcome)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(false)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -269,9 +276,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketTimeOu .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -288,9 +296,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketFailed .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -307,9 +316,10 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButTicketCancel .Times(1) .WillOnce(::testing::Return(outcome)); - AZ_TEST_START_TRACE_SUPPRESSION; + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(1); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchFailure == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -342,13 +352,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallAndTicketComple .WillOnce(::testing::Return(outcome1)) .WillOnce(::testing::Return(outcome2)); - SessionHandlingClientRequestsMock handlerMock; - EXPECT_CALL(handlerMock, RequestPlayerJoinSession(::testing::_)) + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) .Times(1) .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } @@ -365,7 +377,9 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA connectionInfo.SetIpAddress("DummyIpAddress"); connectionInfo.SetPort(123); connectionInfo.AddMatchedPlayerSessions( - Aws::GameLift::Model::MatchedPlayerSession().WithPlayerId("player1").WithPlayerSessionId("playersession1")); + Aws::GameLift::Model::MatchedPlayerSession() + .WithPlayerId("player1") + .WithPlayerSessionId("playersession1")); Aws::GameLift::Model::MatchmakingTicket ticket2; ticket2.SetStatus(Aws::GameLift::Model::MatchmakingConfigurationStatus::COMPLETED); @@ -379,13 +393,15 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_RequiresAcceptanceA .WillOnce(::testing::Return(outcome1)) .WillOnce(::testing::Return(outcome2)); - MatchAcceptanceNotificationsHandlerMock handlerMock1; - EXPECT_CALL(handlerMock1, OnMatchAcceptance()).Times(1); - - SessionHandlingClientRequestsMock handlerMock2; - EXPECT_CALL(handlerMock2, RequestPlayerJoinSession(::testing::_)).Times(1).WillOnce(::testing::Return(true)); + SessionHandlingClientRequestsMock sessionHandlerMock; + EXPECT_CALL(sessionHandlerMock, RequestPlayerJoinSession(::testing::_)) + .Times(1) + .WillOnce(::testing::Return(true)); + MatchmakingNotificationsHandlerMock matchmakingHandlerMock; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish(); + WaitForProcessFinish([this](){ return m_gameliftClientTicketTracker->IsTrackerIdle(); }); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchAcceptance == 1); + ASSERT_TRUE(matchmakingHandlerMock.m_numMatchComplete == 1); ASSERT_TRUE(m_gameliftClientTicketTracker->IsTrackerIdle()); } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp index 1c3e8726fd..6ce0cb8f64 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientManagerTest.cpp @@ -266,6 +266,8 @@ const char* const AWSGameLiftClientManagerTest::DummyPlayerId = "dummyPlayerId"; TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_GetFalseAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AZ_TEST_START_TRACE_SUPPRESSION; auto result = m_gameliftClientManager->ConfigureGameLiftClient(""); AZ_TEST_STOP_TRACE_SUPPRESSION(1); // capture 1 error message @@ -274,6 +276,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutRegion_G TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredential_GetFalseAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AWSResourceMappingRequestsHandlerMock handlerMock; EXPECT_CALL(handlerMock, GetDefaultRegion()).Times(1).WillOnce(::testing::Return("us-west-2")); AZ_TEST_START_TRACE_SUPPRESSION; @@ -284,6 +288,8 @@ TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithoutCredenti TEST_F(AWSGameLiftClientManagerTest, ConfigureGameLiftClient_CallWithRegionAndCredential_GetTrueAsResult) { + AWSCoreRequestsHandlerMock coreHandlerMock; + EXPECT_CALL(coreHandlerMock, GetDefaultConfig()).Times(1).WillOnce(nullptr); AWSCredentialRequestsHandlerMock handlerMock; EXPECT_CALL(handlerMock, GetCredentialsProvider()) .Times(1) diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h index 01afec5c3f..d685f61d30 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientMocks.h @@ -9,9 +9,9 @@ #pragma once #include -#include #include #include +#include #include #include @@ -76,21 +76,44 @@ public: MOCK_METHOD0(OnStopMatchmakingAsyncComplete, void()); }; -class MatchAcceptanceNotificationsHandlerMock - : public AzFramework::MatchAcceptanceNotificationBus::Handler +class MatchmakingNotificationsHandlerMock + : public AzFramework::MatchmakingNotificationBus::Handler { public: - MatchAcceptanceNotificationsHandlerMock() + MatchmakingNotificationsHandlerMock() { - AzFramework::MatchAcceptanceNotificationBus::Handler::BusConnect(); + AzFramework::MatchmakingNotificationBus::Handler::BusConnect(); } - ~MatchAcceptanceNotificationsHandlerMock() + ~MatchmakingNotificationsHandlerMock() { - AzFramework::MatchAcceptanceNotificationBus::Handler::BusDisconnect(); + AzFramework::MatchmakingNotificationBus::Handler::BusDisconnect(); } - MOCK_METHOD0(OnMatchAcceptance, void()); + void OnMatchAcceptance() override + { + ++m_numMatchAcceptance; + } + + void OnMatchComplete() override + { + ++m_numMatchComplete; + } + + void OnMatchError() override + { + ++m_numMatchError; + } + + void OnMatchFailure() override + { + ++m_numMatchFailure; + } + + int m_numMatchAcceptance = 0; + int m_numMatchComplete = 0; + int m_numMatchError = 0; + int m_numMatchFailure = 0; }; class SessionAsyncRequestNotificationsHandlerMock diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake index 629d1596cf..fe22e0c65c 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/awsgamelift_client_files.cmake @@ -17,7 +17,9 @@ set(FILES Include/Request/AWSGameLiftSearchSessionsRequest.h Include/Request/AWSGameLiftStartMatchmakingRequest.h Include/Request/AWSGameLiftStopMatchmakingRequest.h - Include/Request/IAWSGameLiftRequests.h + Include/Request/AWSGameLiftRequestBus.h + Include/Request/AWSGameLiftSessionRequestBus.h + Include/Request/AWSGameLiftMatchmakingRequestBus.h Source/Activity/AWSGameLiftActivityUtils.cpp Source/Activity/AWSGameLiftActivityUtils.h Source/Activity/AWSGameLiftAcceptMatchActivity.cpp diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h similarity index 97% rename from Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h rename to Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h index 777086e633..27096b9fe8 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/IAWSGameLiftServerRequests.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Include/Request/AWSGameLiftServerRequestBus.h @@ -5,7 +5,7 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - + #pragma once #include @@ -45,7 +45,7 @@ namespace AWSGameLift virtual bool StopMatchBackfill(const AZStd::string& ticketId) = 0; }; - // IAWSGameLiftServerRequests EBus wrapper for scripting + // IAWSGameLiftServerRequests EBus wrapper class AWSGameLiftServerRequests : public AZ::EBusTraits { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h index ee21751fe9..6e7ce4e005 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/Source/AWSGameLiftServerManager.h @@ -20,7 +20,7 @@ #include #include -#include +#include namespace AWSGameLift { diff --git a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake index 9039c9943e..70dfd38fc3 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake +++ b/Gems/AWSGameLift/Code/AWSGameLiftServer/awsgamelift_server_files.cmake @@ -10,7 +10,7 @@ set(FILES ../AWSGameLiftCommon/Include/AWSGameLiftPlayer.h ../AWSGameLiftCommon/Source/AWSGameLiftPlayer.cpp ../AWSGameLiftCommon/Source/AWSGameLiftSessionConstants.h - Include/Request/IAWSGameLiftServerRequests.h + Include/Request/AWSGameLiftServerRequestBus.h Source/AWSGameLiftServerManager.cpp Source/AWSGameLiftServerManager.h Source/AWSGameLiftServerSystemComponent.cpp diff --git a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h index 2cd5ba97a8..3bb06acd75 100644 --- a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h +++ b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h @@ -31,7 +31,7 @@ namespace AWSMetrics static const unsigned int DesiredMaxWorkers = 2; MetricsManager(); - ~MetricsManager(); + virtual ~MetricsManager(); //! Initializing the metrics manager //! @return Whether the operation is successful. @@ -93,6 +93,12 @@ namespace AWSMetrics //! @return Total number of requests for sending metrics events. int GetNumTotalRequests() const; + protected: + //! Send metrics to a local file. + //! @param metricsQueue metricsQueue Metrics queue that stores the metrics. + //! @return Outcome of the operation. + virtual AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue); + private: //! Job management void SetupJobContext(); @@ -112,11 +118,6 @@ namespace AWSMetrics //! @param metricsQueue Metrics events to send. void SendMetricsToServiceApiAsync(const MetricsQueue& metricsQueue); - //! Send metrics to a local file. - //! @param metricsQueue metricsQueue Metrics queue that stores the metrics. - //! @return Outcome of the operation. - AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue); - //! Push metrics events to the front of the queue for retry. //! @param metricsEventsForRetry Metrics events for retry. void PushMetricsForRetry(MetricsQueue& metricsEventsForRetry); diff --git a/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h b/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h index b77f39c93c..4f40fd6ff1 100644 --- a/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h +++ b/Gems/AWSMetrics/Code/Tests/AWSMetricsGemMock.h @@ -139,11 +139,6 @@ namespace AWSMetrics return true; } - bool RemoveDirectory(const AZStd::string& directory) - { - return AZ::IO::SystemFile::DeleteDir(directory.c_str()); - } - AZ::IO::FileIOBase* m_priorFileIO = nullptr; AZ::IO::FileIOBase* m_localFileIO = nullptr; diff --git a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp index 3f87a1079d..9fcebec524 100644 --- a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp +++ b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp @@ -75,6 +75,23 @@ namespace AZ namespace AWSMetrics { + class MetricsManagerMock + : public MetricsManager + { + private: + AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue) override + { + if (AZ::IO::FileIOBase::GetInstance()) + { + return AZ::Success(); + } + else + { + return AZ::Failure(AZStd::string{ "Invalid File IO" }); + } + } + }; + class AWSMetricsNotificationBusMock : protected AWSMetricsNotificationBus::Handler { @@ -134,13 +151,11 @@ namespace AWSMetrics AWSMetricsGemAllocatorFixture::SetUp(); AWSMetricsRequestBus::Handler::BusConnect(); - m_metricsManager = AZStd::make_unique(); + m_metricsManager = AZStd::make_unique(); AZStd::string configFilePath = CreateClientConfigFile(true, (double) TestMetricsEventSizeInBytes / MbToBytes * 2, DefaultFlushPeriodInSeconds, 0); m_settingsRegistry->MergeSettingsFile(configFilePath, AZ::SettingsRegistryInterface::Format::JsonMergePatch, {}); m_metricsManager->Init(); - RemoveFile(m_metricsManager->GetMetricsFilePath()); - ReplaceLocalFileIOWithMockIO(); } @@ -149,8 +164,6 @@ namespace AWSMetrics RevertMockIOToLocalFileIO(); RemoveFile(GetDefaultTestFilePath()); - RemoveFile(m_metricsManager->GetMetricsFilePath()); - RemoveDirectory(m_metricsManager->GetMetricsFileDirectory()); m_metricsManager.reset(); @@ -233,7 +246,7 @@ namespace AWSMetrics } } - AZStd::unique_ptr m_metricsManager; + AZStd::unique_ptr m_metricsManager; AWSMetricsNotificationBusMock m_notifications; AZ::IO::FileIOBase* m_fileIOMock; diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl index 099a6394d4..a380a0a547 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl @@ -138,7 +138,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight); frameColor = max(frameColor, 0.0); frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight); - + frameColor = max(frameColor, 0.0); frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight, PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor); frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue); @@ -147,8 +147,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight, PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor); - - frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); return max(frameColor.rgb, 0.0); } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli index 633ea85387..b53dda13aa 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli @@ -111,16 +111,18 @@ void DirectionalLightShadow::GetShadowCoords( float3 worldPosition, out float3 shadowCoords[ViewSrg::MaxCascadeCount]) { - const float4x4 depthBiasMatrices[ViewSrg::MaxCascadeCount] = - ViewSrg::m_directionalLightShadows[lightIndex].m_depthBiasMatrices; const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount; - + const float shadowBias = ViewSrg::m_directionalLightShadows[lightIndex].m_shadowBias; + const float4x4 lightViewToShadowmapMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_lightViewToShadowmapMatrices; + const float4x4 worldToLightViewMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_worldToLightViewMatrices; + for (uint index = 0; index < cascadeCount; ++index) { - const float4x4 depthBiasMatrix = depthBiasMatrices[index]; - const float4 shadowCoordHomogeneous = mul(depthBiasMatrix, - float4(worldPosition, 1.)); - shadowCoords[index] = shadowCoordHomogeneous.xyz / shadowCoordHomogeneous.w; + float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition, 1.)); + lightSpacePos.z += shadowBias; + + const float4 clipSpacePos = mul(lightViewToShadowmapMatrices[index], lightSpacePos); + shadowCoords[index] = clipSpacePos.xyz / clipSpacePos.w; } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli index 94d6f20da3..27fa36182c 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli @@ -102,18 +102,19 @@ partial ShaderResourceGroup ViewSrg struct DirectionalLightShadow { - float4x4 m_depthBiasMatrices[MaxCascadeCount]; float4x4 m_lightViewToShadowmapMatrices[MaxCascadeCount]; float4x4 m_worldToLightViewMatrices[MaxCascadeCount]; float m_slopeBiasBase[MaxCascadeCount]; float m_boundaryScale; uint m_shadowmapSize; // width and height of shadowmap uint m_cascadeCount; + float m_shadowBias; uint m_predictionSampleCount; uint m_filteringSampleCount; uint m_debugFlags; uint m_shadowFilterMethod; float m_far_minus_near; + float3 m_padding; }; enum ShadowFilterMethod diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h index 769c7b95a6..2bba1338a1 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h @@ -157,6 +157,9 @@ namespace AZ //! Sets whether the directional shadowmap should use receiver plane bias. //! This attempts to reduce shadow acne when using large pcf filters. virtual void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) = 0; + + //! Reduces acne by applying a small amount of bias along shadow-space z. + virtual void SetShadowBias(LightHandle handle, float bias) = 0; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index 0a9f3480ad..4774ac31a1 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -589,6 +589,15 @@ namespace AZ m_shadowProperties.GetData(handle.GetIndex()).m_isReceiverPlaneBiasEnabled = enable; } + void DirectionalLightFeatureProcessor::SetShadowBias(LightHandle handle, float bias) + { + for (auto& it : m_shadowData) + { + it.second.GetData(handle.GetIndex()).m_shadowBias = bias; + } + m_shadowBufferNeedsUpdate = true; + } + void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) { PrepareForChangingRenderPipelineAndCameraView(); @@ -1522,10 +1531,6 @@ namespace AZ for (uint16_t cascadeIndex = 0; cascadeIndex < GetCascadeCount(handle); ++cascadeIndex) { - const Matrix4x4& worldToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetWorldToClipMatrix(); - const Matrix4x4 depthBiasMatrix = Shadow::GetClipToShadowmapTextureMatrix() * worldToLightClipMatrix; - shadowData.m_depthBiasMatrices[cascadeIndex] = depthBiasMatrix; - const Matrix4x4& lightViewToLightClipMatrix = property.m_segments.at(cameraView)[cascadeIndex].m_view->GetViewToClipMatrix(); const Matrix4x4 lightViewToShadowmapMatrix = Shadow::GetClipToShadowmapTextureMatrix() * lightViewToLightClipMatrix; shadowData.m_lightViewToShadowmapMatrices[cascadeIndex] = lightViewToShadowmapMatrix; diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h index 3c1ff8eabd..2cf5b0b1e6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h @@ -72,12 +72,6 @@ namespace AZ // [GFX TODO][ATOM-15172] Look into compacting struct DirectionalLightShadowData struct DirectionalLightShadowData { - AZStd::array m_depthBiasMatrices = - { { - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity(), - Matrix4x4::CreateIdentity() } }; AZStd::array m_lightViewToShadowmapMatrices = { { Matrix4x4::CreateIdentity(), @@ -97,11 +91,14 @@ namespace AZ float m_boundaryScale = 0.f; uint32_t m_shadowmapSize = 1; // width and height of shadowmap uint32_t m_cascadeCount = 1; + // Reduce acne by applying a small amount of bias to apply along shadow-space z. + float m_shadowBias = 0.0f; uint32_t m_predictionSampleCount = 0; uint32_t m_filteringSampleCount = 0; uint32_t m_debugFlags = 0; uint32_t m_shadowFilterMethod = 0; float m_far_minus_near = 0; + float m_padding[3]; }; class DirectionalLightFeatureProcessor final @@ -218,6 +215,7 @@ namespace AZ void SetShadowFilterMethod(LightHandle handle, ShadowFilterMethod method) override; void SetFilteringSampleCount(LightHandle handle, uint16_t count) override; void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override; + void SetShadowBias(LightHandle handle, float bias) override; const Data::Instance GetLightBuffer() const; uint32_t GetLightCount() const; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt b/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt index ea5b65be7c..e64c9b80e7 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt @@ -10,6 +10,8 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS) return() endif() +ly_get_list_relative_pal_filename(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME}) + ly_add_target( NAME AtomToolsFramework.Static STATIC NAMESPACE Gem @@ -18,9 +20,11 @@ ly_add_target( AUTORCC FILES_CMAKE atomtoolsframework_files.cmake + ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake INCLUDE_DIRECTORIES PRIVATE Source + ${pal_source_dir} PUBLIC Include BUILD_DEPENDENCIES diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp index 3fae2ee7a3..58b07d8351 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp @@ -216,7 +216,11 @@ namespace AtomToolsFramework AtomToolsMainWindowNotificationBus::Handler::BusDisconnect(); AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); +#if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY + _exit(0); +#else Base::Destroy(); +#endif } AZStd::vector AtomToolsApplication::GetCriticalAssetFilters() const diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h new file mode 100644 index 0000000000..2f4c787fbe --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Linux.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY true + diff --git a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h similarity index 70% rename from Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp rename to Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h index 4344eb6072..8101a49a5a 100644 --- a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/AtomToolsFramework_Traits_Platform.h @@ -5,7 +5,6 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ +#pragma once -#include - -AZ_DECLARE_MODULE_CLASS(Gem_AudioEngineWwise, AZ::Module) +#include diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake new file mode 100644 index 0000000000..957ef8663e --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Linux/platform_linux_files.cmake @@ -0,0 +1,12 @@ +# +# 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 +# +# + +set(FILES + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Linux.h +) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h new file mode 100644 index 0000000000..3ca246c797 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Mac.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false + diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h new file mode 100644 index 0000000000..6d8c8d7e32 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/AtomToolsFramework_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * 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 diff --git a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake similarity index 74% rename from Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake rename to Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake index 5597c28d04..13cc6886ef 100644 --- a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Mac/platform_mac_files.cmake @@ -7,5 +7,6 @@ # set(FILES - Source/AudioEngineWwiseModule_Stub.cpp + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Mac.h ) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h new file mode 100644 index 0000000000..ac82be7874 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Platform.h @@ -0,0 +1,10 @@ +/* + * 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 diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h new file mode 100644 index 0000000000..3ca246c797 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/AtomToolsFramework_Traits_Windows.h @@ -0,0 +1,15 @@ +/* + * 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 + +// On some platforms, there is an issue with environment variables that are removed before some objects are deallocated (during the process of +// ComponentApplication::Destroy). Until all of the shutdown issues are solved, the following trait will skip the parent ::Destroy() and exit +// the application as soon as possible if set to true. +// (Tracked by GHI - 4806) +#define AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY false + diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake new file mode 100644 index 0000000000..e2a2113bf9 --- /dev/null +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Platform/Windows/platform_windows_files.cmake @@ -0,0 +1,12 @@ +# +# 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 +# +# + +set(FILES + AtomToolsFramework_Traits_Platform.h + AtomToolsFramework_Traits_Windows.h +) diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h index a8088c63ac..9ccc4f329b 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h @@ -168,6 +168,14 @@ namespace AZ //! Sets whether the directional shadowmap should use receiver plane bias. //! @param enable flag specifying whether to enable the receiver plane bias feature virtual void SetShadowReceiverPlaneBiasEnabled(bool enable) = 0; + + //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. + //! @return Returns the amount of bias to apply. + virtual float GetShadowBias() const = 0; + + //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. + //! @param Sets the amount of bias to apply. + virtual void SetShadowBias(float bias) = 0; }; using DirectionalLightRequestBus = EBus; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h index a58acc0114..20073f437c 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h @@ -109,6 +109,9 @@ namespace AZ //! This uses partial derivatives to reduce shadow acne when using large pcf kernels. bool m_receiverPlaneBiasEnabled = true; + //! Reduces shadow acne by applying a small amount of offset along shadow-space z. + float m_shadowBias = 0.0f; + bool IsSplitManual() const; bool IsSplitAutomatic() const; bool IsCascadeCorrectionDisabled() const; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp index 9d384c2e24..98d3f838c0 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp @@ -38,7 +38,8 @@ namespace AZ ->Field("IsDebugColoringEnabled", &DirectionalLightComponentConfig::m_isDebugColoringEnabled) ->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod) ->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount) - ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled); + ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled) + ->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp index 78558cfc85..c20a9f8e17 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp @@ -84,6 +84,8 @@ namespace AZ ->Event("SetFilteringSampleCount", &DirectionalLightRequestBus::Events::SetFilteringSampleCount) ->Event("GetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::GetShadowReceiverPlaneBiasEnabled) ->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled) + ->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias) + ->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias) ->VirtualProperty("Color", "GetColor", "SetColor") ->VirtualProperty("Intensity", "GetIntensity", "SetIntensity") ->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter") @@ -98,7 +100,8 @@ namespace AZ ->VirtualProperty("DebugColoringEnabled", "GetDebugColoringEnabled", "SetDebugColoringEnabled") ->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod") ->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount") - ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled"); + ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled") + ->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias"); ; } } @@ -406,6 +409,20 @@ namespace AZ return aznumeric_cast(m_configuration.m_filteringSampleCount); } + void DirectionalLightComponentController::SetShadowBias(float bias) + { + m_configuration.m_shadowBias = bias; + if (m_featureProcessor) + { + m_featureProcessor->SetShadowBias(m_lightHandle, bias); + } + } + + float DirectionalLightComponentController::GetShadowBias() const + { + return m_configuration.m_shadowBias; + } + void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count) { const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast(count)); @@ -499,6 +516,7 @@ namespace AZ SetViewFrustumCorrectionEnabled(m_configuration.m_isCascadeCorrectionEnabled); SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled); SetShadowFilterMethod(m_configuration.m_shadowFilterMethod); + SetShadowBias(m_configuration.m_shadowBias); SetFilteringSampleCount(m_configuration.m_filteringSampleCount); SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h index 933f2705e7..a0d552cf99 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h @@ -80,6 +80,8 @@ namespace AZ void SetFilteringSampleCount(uint32_t count) override; bool GetShadowReceiverPlaneBiasEnabled() const override; void SetShadowReceiverPlaneBiasEnabled(bool enable) override; + float GetShadowBias() const override; + void SetShadowBias(float width) override; private: friend class EditorDirectionalLightComponent; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp index 69ba295e9b..99e2cc1485 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp @@ -133,8 +133,8 @@ namespace AZ ->EnumAttribute(ShadowFilterMethod::Esm, "ESM") ->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) - ->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count", - "This is used only when the pixel is predicted as on the boundary. " + ->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n", + "This is used only when the pixel is predicted as on the boundary.\n" "Specific to PCF and ESM+PCF.") ->Attribute(Edit::Attributes::Min, 4) ->Attribute(Edit::Attributes::Max, 64) @@ -142,10 +142,19 @@ namespace AZ ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled) ->DataElement( Edit::UIHandlers::CheckBox, &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled, - "Shadow Receiver Plane Bias Enable", + "Shadow Receiver Plane Bias Enable\n", "This reduces shadow acne when using large pcf kernels.") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) - ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled); + ->Attribute(Edit::Attributes::ReadOnly, &DirectionalLightComponentConfig::IsShadowPcfDisabled) + ->DataElement( + Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_shadowBias, + "Shadow Bias\n", + "Reduces acne by applying a fixed bias along z in shadow-space.\n" + "If this is 0, no biasing is applied.") + ->Attribute(Edit::Attributes::Min, 0.f) + ->Attribute(Edit::Attributes::Max, 0.2) + ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) + ; } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp index 746b1ff7e5..e53010d747 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp @@ -194,6 +194,8 @@ namespace AZ AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultLightingPresetPath), propertyOverrides), [entityId, materialAssignmentId]() { + AZ_UNUSED(entityId); + AZ_UNUSED(materialAssignmentId); AZ_Warning( "EditorMaterialSystemComponent", false, "RenderMaterialPreview capture failed for entity %s slot %s.", entityId.ToString().c_str(), materialAssignmentId.ToString().c_str()); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp index 2d8f83a32a..e32b26f0c5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp @@ -131,16 +131,20 @@ namespace AZ ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsStart, "Shadows Start", "SMH Shadows Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsEnd, "Shadows End", "SMH Shadows End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsStart, "Highlights Start", "SMH Highlights Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsEnd, "Highlights End", "SMH Highlights End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhShadowsColor, "Shadows Color", "SMH Shadows Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhMidtonesColor, "Midtones Color", "SMH Midtones Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhHighlightsColor, "Highlights Color", "SMH Highlights Color") diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass index 84bb85c3e1..9bfa746bf1 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass @@ -212,7 +212,11 @@ // instead of regular Depth as DepthStencil. Specifically, HairResolvePPLL.pass and the associated // .azsl file will need to be updated. "Name": "HairParentPass", - "TemplateName": "HairParentPassTemplate", + // Note: The following two lines represent the choice of rendering pipeline for the hair. + // You can either choose to use PPLL or ShortCut and accordingly change the flag + // 'm_usePPLLRenderTechnique' in the class 'HairFeatureProcessor.cpp' +// "TemplateName": "HairParentPassTemplate", + "TemplateName": "HairParentShortCutPassTemplate", "Enabled": true, "Connections": [ // Critical to keep DepthLinear as input - used to set the size of the Head PPLL image buffer. diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset index a287664286..1580a4e1a9 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset @@ -8,6 +8,11 @@ "Name": "HairParentPassTemplate", "Path": "Passes/HairParentPass.pass" }, + { + "Name": "HairParentShortCutPassTemplate", + "Path": "Passes/HairParentShortCutPass.pass" + }, + { "Name": "HairGlobalShapeConstraintsComputePassTemplate", "Path": "Passes/HairGlobalShapeConstraintsCompute.pass" @@ -32,6 +37,7 @@ "Name": "HairUpdateFollowHairComputePassTemplate", "Path": "Passes/HairUpdateFollowHairCompute.pass" }, + { "Name": "HairPPLLRasterPassTemplate", "Path": "Passes/HairFillPPLL.pass" @@ -39,6 +45,23 @@ { "Name": "HairPPLLResolvePassTemplate", "Path": "Passes/HairResolvePPLL.pass" + }, + + { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "Path": "Passes/HairShortCutGeometryDepthAlpha.pass" + }, + { + "Name": "HairShortCutResolveDepthPassTemplate", + "Path": "Passes/HairShortCutResolveDepth.pass" + }, + { + "Name": "HairShortCutGeometryShadingPassTemplate", + "Path": "Passes/HairShortCutGeometryShading.pass" + }, + { + "Name": "HairShortCutResolveColorPassTemplate", + "Path": "Passes/HairShortCutResolveColor.pass" } ] } diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass new file mode 100644 index 0000000000..7322611e2a --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass @@ -0,0 +1,391 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairParentShortCutPassTemplate", + "PassClass": "ParentPass", + "Slots": [ + { + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + }, + { // used for copy from MSAA to regular RT + "Name": "RenderTargetInputOnly", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + // This is the depth stencil buffer that is to be used by the fill pass + // to early reject pixels by depth and in the resolve pass to write the + // the hair depth + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" + }, + // Keep DepthLinear as input - used to set the size of the Head PPLL image buffer. + // If DepthLinear is not availbale - connect to another viewport (non MSAA) image. + { + "Name": "DepthLinearInput", + "SlotType": "InputOutput" + }, + { + "Name": "DepthLinear", + "SlotType": "Output" + }, + + // Lights & Shadows resources + { + "Name": "DirectionalShadowmap", + "SlotType": "Input" + }, + { + "Name": "DirectionalESM", + "SlotType": "Input" + }, + { + "Name": "ProjectedShadowmap", + "SlotType": "Input" + }, + { + "Name": "ProjectedESM", + "SlotType": "Input" + }, + { + "Name": "TileLightData", + "SlotType": "Input" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input" + } + ], + "Connections": [ + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "DepthToDepthLinearPass", + "Attachment": "Output" + } + } + ], + "PassRequests": [ + { + "Name": "HairGlobalShapeConstraintsComputePass", + "TemplateName": "HairGlobalShapeConstraintsComputePassTemplate", + "Enabled": true + }, + { + "Name": "HairCalculateStrandLevelDataComputePass", + "TemplateName": "HairCalculateStrandLevelDataComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairGlobalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairVelocityShockPropagationComputePass", + "TemplateName": "HairVelocityShockPropagationComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairCalculateStrandLevelDataComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLocalShapeConstraintsComputePass", + "TemplateName": "HairLocalShapeConstraintsComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairVelocityShockPropagationComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLengthConstraintsWindAndCollisionComputePass", + "TemplateName": "HairLengthConstraintsWindAndCollisionComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLocalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairUpdateFollowHairComputePass", + "TemplateName": "HairUpdateFollowHairComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLengthConstraintsWindAndCollisionComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + + // Render Target Copy from MS to Regular + { + "Name": "RenderTargetCopyPass", + "TemplateName": "FullscreenCopyTemplate", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOnly" + } + }, + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "This", + "Attachment": "Output" + } + } + ], + "ImageAttachments": [ + { + "Name": "Output", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "Input" + } + }, + "FormatSource": { + "Pass": "This", + "Attachment": "Input" + }, + "GenerateFullMipChain": false + } + ] + }, + + // Rendering Passes + { + "Name": "HairShortCutGeometryDepthAlphaPass", + "TemplateName": "HairShortCutGeometryDepthAlphaPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "InverseAlphaRTOutput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutResolveDepthPass", + "TemplateName": "HairShortCutResolveDepthPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutGeometryShadingPass", + "TemplateName": "HairShortCutGeometryShadingPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "HairColorRenderTarget", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairColorRenderTarget" + } + }, + { // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DepthLinearInput" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + + // Shadows resources + { + "LocalSlot": "DirectionalShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalShadowmap" + } + }, + { + "LocalSlot": "DirectionalESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalESM" + } + }, + { + "LocalSlot": "ProjectedShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedShadowmap" + } + }, + { + "LocalSlot": "ProjectedESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedESM" + } + }, + + // Lights Resources + { + "LocalSlot": "TileLightData", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "TileLightData" + } + }, + { + "LocalSlot": "LightListRemapped", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightListRemapped" + } + } + ] + }, + + { + "Name": "HairShortCutResolveColorPass", + "TemplateName": "HairShortCutResolveColorPassTemplate", + "Enabled": true, + "Connections": [ + { // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "AccumulatedInverseAlpha", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairColorTexture", + "AttachmentRef": { + "Pass": "HairShortCutGeometryShadingPass", + "Attachment": "HairColorRenderTarget" + } + } + ] + }, + + { + // This pass copies the updated depth buffer (now contains hair depth) to linear depth texture + // for downstream passes to use. This can be optimized even further by writing into the stencil + // buffer pixels that were touched by HairPPLLResolvePass hence preventing depth update unless + // it is hair. + "Name": "DepthToDepthLinearPass", + "TemplateName": "DepthToLinearTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "HairShortCutResolveDepthPass", + "Attachment": "Depth" + } + } + ] + } + + ] + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass new file mode 100644 index 0000000000..559224faa6 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass @@ -0,0 +1,101 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "PassClass": "HairShortCutGeometryDepthAlphaPass", + "Slots": [ + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // DepthStencil for early disqualifying the pixel based on depth. No write. + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + // the regular render target is blended using inverse alpha to reduce the + // incoming color contribution based on the hair thickness and alpha. + "Name": "InverseAlphaRTOutput", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { + "Value": [ 1.0, 1.0, 1.0, 1.0 ] + }, + "StoreAction": "Store" + } + }, + { + "Name": "HairDepthsTextureArray", + "SlotType": "Output", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_RWFragmentDepthsTexture", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + } + ], + "ImageAttachments": [ + { + // This buffer is used as the render target and should be at non-MSAA screen resolution + // to make sure no overwork is done. + "Name": "InverseAlphaRTOutput", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "HairDepthsTextureArray", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_UINT", + "ArraySize": "3", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "ShaderReadWrite", + "ShaderWrite", + "ShaderRead" + ] + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryDepthAlphaDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryDepthAlpha.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass new file mode 100644 index 0000000000..5940f8c549 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass @@ -0,0 +1,155 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryShadingPassTemplate", + "PassClass": "HairShortCutGeometryShadingPass", + "Slots": [ + + { // Temporary color buffer to store the gathered shaded hair color - MSAA + "Name": "HairColorRenderTarget", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + }, + + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // Used to get the transform from screen space to world space. + "Name": "DepthLinear", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // For comparing the depth to early disqualify but not to write + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + + //------------- Shadowing Resources ------------- + { + "Name": "DirectionalShadowmap", + "ShaderInputName": "m_directionalLightShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "DirectionalESM", + "ShaderInputName": "m_directionalLightExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedShadowmap", + "ShaderInputName": "m_projectedShadowmaps", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedESM", + "ShaderInputName": "m_projectedExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + + //------------- Lighting Resources ------------- + { + "Name": "BRDFTextureInput", + "ShaderInputName": "m_brdfMap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "TileLightData", + "SlotType": "Input", + "ShaderInputName": "m_tileLightData", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input", + "ShaderInputName": "m_lightListRemapped", + "ScopeAttachmentUsage": "Shader" + } + + ], + "ImageAttachments": [ + { + // The shader hair color render target - important to have at a non-MSAA mode + // so that no overwork is done on sampling. + "Name": "HairColorRenderTarget", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "BRDFTexture", + "Lifetime": "Imported", + "AssetRef": { + "FilePath": "Textures/BRDFTexture.attimage" + } + } + ], + "Connections": [ + { + "LocalSlot": "BRDFTextureInput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "BRDFTexture" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryShadingDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryShading.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass new file mode 100644 index 0000000000..efbf993307 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass @@ -0,0 +1,45 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveColorPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { + "Name": "HairColorTexture", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_hairColorTexture" + }, + { + "Name": "AccumulatedInverseAlpha", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_accumInvAlpha" + } + ], + "Connections": [ + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveColor.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass new file mode 100644 index 0000000000..021d711f1f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass @@ -0,0 +1,37 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveDepthPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + //------ General Input/Output resources and Render Target ------ + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { // This holds the K nearset depths. The furthest depth will be taken to be written in the depth buffer. + "Name": "HairDepthsTextureArray", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_fragmentDepthsTexture" + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveDepth.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli index cb0bdc2c6c..a4568588d5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli @@ -29,7 +29,7 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ diff --git a/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli new file mode 100644 index 0000000000..7e57e8102c --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli @@ -0,0 +1,60 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include +#include + +//============================================================================== +// Generate a fullscreen triangle from pipeline provided vertex id +VSOutput FullScreenVS(VSInput input) +{ + VSOutput OUT; + + float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); + + OUT.m_texCoord = float2(posTex.z, posTex.w); // [To Do] - test sign of Y based on original code + OUT.m_position = float4(posTex.xy, 0.0, 1.0); + + return OUT; +} + +//============================================================================== +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the NDC. +// screenCoords - from 0.. dimension of the screen of the current pixel +// screenTexture - screen buffer texture representing the same resolution we work in +// sDepth - the depth buffer depth at the fragment location +// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) +float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) +{ + uint2 dimensions; + screenTexture.GetDimensions(dimensions.x, dimensions.y); + float2 UV = saturate(screenCoords / dimensions.xy); + + float x = UV.x * 2.0f - 1.0f; + float y = (1.0f - UV.y) * 2.0f - 1.0f; + float3 NDC = float3(x, y, depth); + + return NDC; +} + +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the world space position +float3 ScreenPosToWorldPos( + Texture2D screenTexture, float2 screenCoords, float depth, + inout float3 screenPosNDC ) +{ + screenPosNDC = ScreenPosToNDC(screenTexture, screenCoords, depth); + float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] + float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); + positionVS /= positionVS.w; // notice the normalization factor - crucial! + float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); + + return positionWS.xyz; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli index dcdcf43243..7beba248f7 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli @@ -230,7 +230,7 @@ float3 CalculateLighting( return lightingData.diffuseLighting + lightingData.specularLighting; } -float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, float3 baseColor, float thickness, int shaderParamIndex) +float3 TressFXShading(float2 pixelCoord, float depth, float3 tangent, float3 baseColor, float thickness, int shaderParamIndex) { float3 vNDC; // normalized device / screen coordinates: [-1..1, -1..1, 0..1] float3 vPositionWS = ScreenPosToWorldPos(PassSrg::m_linearDepth, pixelCoord, depth, vNDC); @@ -241,9 +241,6 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f float3 vViewDirWS = g_vEye - vPositionWS; - // Need to expand the tangent that was compressed to store in the buffer - float3 vTangent = normalize(vTangentCoverage.xyz * 2.f - 1.f); - //---- TressFX original lighting params setting ---- HairShadeParams params; params.m_color = baseColor; @@ -266,11 +263,19 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f if (o_hairLightingModel == HairLightingModel::Kajiya) { // This option should be removed and the Kajiya-Kay model should be operated from within // the Atom lighting loop. - accumulatedLight = SimplifiedHairLighting(vTangent, vPositionWS, vViewDirWS, params, vNDC); + accumulatedLight = SimplifiedHairLighting(tangent, vPositionWS, vViewDirWS, params, vNDC); } else { - accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, vTangent, thickness, params); + accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, tangent, thickness, params); } return accumulatedLight; } + +float3 TressFXShadingFullScreen(float2 pixelCoord, float depth, float3 compressedTangent, float3 baseColor, float thickness, int shaderParamIndex) +{ + // The tangent that was compressed to store in the PPLL structure + float3 tangent = normalize(compressedTangent.xyz * 2.f - 1.f); + + return TressFXShading(pixelCoord, depth, tangent, baseColor, thickness, shaderParamIndex); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli index 83ebc04521..4e4245f765 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli @@ -66,7 +66,7 @@ option bool o_enableAzimuthCoeff = true; float M_R(Surface surface, float Lh, float sinLiPlusSinLr) { float a = 1.0f * surface.cuticleTilt; // Tilt is translate as the mean offset - float b = 0.5 * surface.roughnessA2; // Roughness is used as the standard deviation + float b = 0.5f * surface.roughnessA2; // Roughness is used as the standard deviation // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -74,8 +74,8 @@ float M_R(Surface surface, float Lh, float sinLiPlusSinLr) float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.0 * surface.cuticleTilt; - float b = 0.5 * surface.roughnessA2; + float a = 1.0f * surface.cuticleTilt; + float b = 0.5f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -83,8 +83,8 @@ float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) float M_TRT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.5 * surface.cuticleTilt; - float b = 1.0 * surface.roughnessA2; + float a = 1.5f * surface.cuticleTilt; + float b = 1.0f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl index 82c677faad..02777435f5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl @@ -40,7 +40,8 @@ //! that can change between passes due to the application of skinning, simulation //! and physics affect and is then read by the rendering shaders. ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback -{ //! This shared buffer needs to match the SharedBuffer structure +{ + //! This shared buffer needs to match the SharedBuffer structure //! shared between all draw calls / dispatches for the hair skinning StructuredBuffer m_skinnedHairSharedBuffer; @@ -101,39 +102,8 @@ ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance #define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents //============================================================================== -#include +#include // VS resides here //============================================================================== -//! Hair input structure to Pixel shaders -struct PS_INPUT_HAIR -{ - float4 Position : SV_POSITION; - float4 Tangent : Tangent; - float4 p0p1 : TEXCOORD0; - float4 StrandColor : TEXCOORD1; -}; - -//! Hair Render VS -PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) -{ -// uint2 scrSize; -// PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); -// TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); - - // [To Do] Hair: the above code should replace the existing but requires modifications to - // the function GetExpandedTressFXVert. - // Note that in Atom g_vViewport is aspect ratio and NOT size. - TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); - - - PS_INPUT_HAIR Output; - - Output.Position = tressfxVert.Position; - Output.Tangent = tressfxVert.Tangent; - Output.p0p1 = tressfxVert.p0p1; - Output.StrandColor = tressfxVert.StrandColor; - - return Output; -} // Allocate a new fragment location in fragment color, depth, and link buffers int AllocateFragment(int2 vScreenAddress) @@ -202,9 +172,9 @@ void PPLLFillPS(PS_INPUT_HAIR input) ////////////////////////////////////////////////////////////////////// // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now - float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); - uint2 dimensions; - PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); // float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); float coverage = 1.0; ///////////////////////////////////////////////////////////////////// diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl index 82c75280f6..3d72f1e21e 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl @@ -58,7 +58,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback // in the OIT process. // It can also be used to avoid the HW blend done at the end of the pixel // shader stage but HW blend might be cheaper than additional PS blend. - Texture2D m_frameBuffer; // The merged MSAA output + Texture2D m_frameBuffer; // The merged non-MSAA input // Linear depth is used for getting the screen to world transform Texture2D m_linearDepth; @@ -93,26 +93,11 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback #define HairParams PassSrg::m_hairParams //============================================================================== +#include // provides the Vertex Shader #include -#include -#include - -// Generates a fullscreen triangle from pipeline provided vertex id -VSOutput FullScreenVS(VSInput input) -{ - VSOutput OUT; - - float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); - - OUT.m_texCoord = float2(posTex.z, posTex.w); - OUT.m_position = float4(posTex.x, posTex.y, 0.0, 1.0); - - return OUT; -} ////////////////////////////////////////////////////////////// // Bind data for PPLLResolvePS - #define NODE_DATA(x) LinkedListNodes[x].data #define NODE_NEXT(x) LinkedListNodes[x].uNext #define NODE_DEPTH(x) LinkedListNodes[x].depth @@ -298,7 +283,7 @@ float4 GatherLinkedList(float2 vfScreenAddress, float2 screenUV, inout float out uint shadeParamIndex; // So we know what settings to shade with float3 vColor = UnpackUintIntoFloat3Byte(color, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + fragmentColor * alpha; @@ -355,7 +340,7 @@ float4 GetClosestFragment(float2 vfScreenAddress, float2 screenUV, inout float c float alpha = 1.0; uint shadeParamIndex; // the material index float3 vColor = UnpackUintIntoFloat3Byte(curColor, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + (fragmentColor * alpha); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl new file mode 100644 index 0000000000..23c926406f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl @@ -0,0 +1,131 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Based on [[vk::binding(0, 3)]] RWTexture2DArray RWFragmentDepthsTexture : register(u0, space3); + RWTexture2DArray m_RWFragmentDepthsTexture; +} +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here + +//!============================================================================= +//! Geometry Depth Alpha - First Pass of ShortCut Render +//! It is a Geometry pass that stores the K=3 front fragment depths, and accumulates +//! product of 1-alpha multiplications (fade out) of the input render target. +//! +//! Short explanation: in the original AMD implementation 1-alpha is multiplied +//! repeatedly with the incoming render target (back buffer) hence blending out +//! the existing back buffer color based on the density and transparency of the hair. +//! This implies that later on the hair color should be added based on the inverse +//! of this operation. +//!============================================================================= +[earlydepthstencil] +float HairShortCutDepthsAlphaPS(PS_INPUT_HAIR input) : SV_Target +{ + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage * MatBaseColor.a; + + if (alpha < SHORTCUT_MIN_ALPHA) + return 1.0; + + int2 vScreenAddress = int2(input.Position.xy); + uint uDepth = asuint(input.Position.z); + uint uDepth0Prev, uDepth1Prev, uDepth2Prev; + + // Min of depth 0 and input depth - in Atom the Z order is reverse + // Original value is uDepth0Prev + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 0)], uDepth, uDepth0Prev); + + // Min of depth 1 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth0Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 1)], uDepth, uDepth1Prev); + + // Min of depth 2 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth1Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 2)], uDepth, uDepth2Prev); + + // Accumulate the alpha multiplication from all hair components by multiplying the inverse and + // therefore going down towards 0. At the end product, the inverse will be taken as the hair + // alpha and the remainder will be used to blend the back buffer. + return 1.0 - alpha; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader new file mode 100644 index 0000000000..7cd1f44510 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryDepthAlpha.azsl", + "DrawList" : "HairGeometryDepthAlphaDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "Zero", + "BlendDest" : "ColorSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "AlphaSource", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutDepthsAlphaPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl new file mode 100644 index 0000000000..c2a2958dfe --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl @@ -0,0 +1,176 @@ +/* +* Modifications 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) AND MIT +* +*/ + +//------------------------------------------------------------------------------ +// Shader code related to lighting and shadowing for TressFX +//------------------------------------------------------------------------------ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +#define AMD_TRESSFX_MAX_HAIR_GROUP_RENDER 16 + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Per hair object material array used by the PPLL resolve pass + //! Originally in TressFXRendering.hlsl this is space 0 + HairObjectShadeParams m_hairParams[AMD_TRESSFX_MAX_HAIR_GROUP_RENDER]; + + // Linear depth is used for getting the screen to world transform + Texture2D m_linearDepth; + + //------------------------------ + // Lighting Data + //------------------------------ + Sampler LinearSampler + { // Required by LightingData.azsli + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + Texture2DArray m_directionalLightShadowmap; + Texture2DArray m_directionalLightExponentialShadowmap; + Texture2DArray m_projectedShadowmaps; + Texture2DArray m_projectedExponentialShadowmap; + Texture2D m_brdfMap; + Texture2D m_tileLightData; + StructuredBuffer m_lightListRemapped; +} + +//------------------------------------------------------------------------------ +//! The hair objects' material array buffer used by the rendering resolve pass +#define HairParams PassSrg::m_hairParams +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here +#include // Required for world coordinates calculation +#include + +//!============================================================================= +//! Geometry Shading - Third Pass of ShortCut Render +//! Geometry pass that shades pixels that passes the early depth test. Due to this, it +//! is limited to the stored K near fragments due to previous depth write pass that +//! wrote the furthest depth of the K stored depths. +//! Colors are accumulated in the render target for a weighted average in final pass. +//! [To Do] - in the original short cut, the alpha is taken from the depth alpha pass +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutGeometryColorPS(PS_INPUT_HAIR input) : SV_Target +{ + // Strand Color read in is either the BaseMatColor, or BaseMatColor modulated with a color read from texture + // on vertex shader for base color along with modulation by the tip color + float4 strandColor = float4(input.StrandColor.rgb, MatBaseColor.a); + + // If we are supporting strand UV texturing, further blend in the texture color/alpha + // Do this while computing NDC and coverage to hide latency from texture lookup + if (EnableStrandUV) + { + // Grab the uv in case we need it + float2 uv = float2(input.Tangent.w, input.StrandColor.w); + + // Apply StrandUVTiling + float2 strandUV = float2(uv.x, (uv.y * StrandUVTilingFactor) - floor(uv.y * StrandUVTilingFactor)); + + strandColor.rgb *= StrandAlbedoTexture.Sample(LinearWrapSampler, strandUV).rgb; + } + + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float2 screenCoords = saturate(pixelCoord / dimensions.xy); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); +// original: float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, g_vViewport.zw - g_vViewport.xy); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage; + + // Update the alpha to have proper value (accounting for coverage, base alpha, and strand alpha) + alpha *= strandColor.w; + + // Early out + if (alpha < SHORTCUT_MIN_ALPHA) + { + return float4(0, 0, 0, 0); + } + + float2 pixelCoord = input.Position.xy; + float depth = input.Position.z; + // [To Do] - the thickness will need to be corrected somehow since this technique doesn't + // keeps track of the accumulated alpha / thickness + float thickness = alpha; + float3 shadedFragment = TressFXShading(pixelCoord, depth, input.Tangent.xyz, strandColor.rgb, thickness, RenderParamsIndex); + + // Color channel: Pre-multiply with alpha to create non-normalized weighted sum. + // Alpha Channel: Sum up all the hair alphas - this will be used to normalize the color + // per fragment at the next pass. + return float4(shadedFragment * alpha, alpha); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader new file mode 100644 index 0000000000..40e56006cd --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryShading.azsl", + "DrawList" : "HairGeometryShadingDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "One", + "BlendOp" : "Add", + "BlendAlphaSource" : "One", + "BlendAlphaDest" : "One", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutGeometryColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl new file mode 100644 index 0000000000..f25c9dd711 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl @@ -0,0 +1,63 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // oiriginally: [[vk::binding(0, 0)]] Texture2D HaiColorTexture : register(t0, space0); + // oiriginally: [[vk::binding(1, 0)]] Texture2D AccumInvAlpha : register(t1, space0); + Texture2D m_hairColorTexture; + Texture2D m_accumInvAlpha; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! HairColorPS - Fourth Pass of ShortCut Render +//! Full-screen pass that finalizes the weighted average, and blends using the +//! accumulated 1-alpha product. +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutResolveColorPS(VSOutput input) : SV_Target +{ + int2 vScreenAddress = int2(input.m_position.xy); + + float fInvAlpha = PassSrg::m_accumInvAlpha[vScreenAddress]; + float fAlpha = 1.0 - fInvAlpha; + + if (fAlpha < SHORTCUT_MIN_ALPHA) + { + // next we discard of non-hair pixels to avoid manipulating them depending + // on the alpha blend state - this is the safer and faster approach as there + // is no hair in these pixels + discard; + } + + float4 finalColor; + float weightSum = PassSrg::m_hairColorTexture[vScreenAddress].w; + + // Normalize the sum of the shaded fragment from the previous pass and + // then multiply it by the alpha blend of the hairs done in the depth-alpha pass. + finalColor.xyz = PassSrg::m_hairColorTexture[vScreenAddress] * fAlpha / weightSum; + + // The alpha is set to the inverse alpha of the hair so that the original + // background will be blended using this factor emulating single step alpha blend + // over the sum of all hair fragment blends. + finalColor.w = fInvAlpha; + + return finalColor; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader new file mode 100644 index 0000000000..6703c63f18 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader @@ -0,0 +1,41 @@ +{ + "Source" : "HairShortCutResolveColor.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : false // Avoid comparing depth + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "AlphaSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "Zero", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl new file mode 100644 index 0000000000..862b7c9015 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl @@ -0,0 +1,65 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // Originally: [[vk::binding(0, 0)]] Texture2DArray FragmentDepthsTexture : register(t0, space0); + Texture2DArray m_fragmentDepthsTexture; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! Resolve Depth - Second Pass of ShortCut +//! Full-screen pass that writes the farthest of the stored K near depths so it +//! could be used for depth culling during the following geometry shading pass. +//!============================================================================= +float HairShortCutResolveDepthPS(VSOutput input) : SV_Depth +{ + // Blend the layers of fragments from back to front + int2 vScreenAddress = int2(input.m_position.xy); + + // Write farthest depth value for culling in the next pass. + // It may be the initial value of 1.0 if there were not enough fragments to write all depths, but then culling not important. + const int farthestDepthIndex = 2; + uint uDepth = PassSrg::m_fragmentDepthsTexture[uint3(vScreenAddress, farthestDepthIndex)]; + + // The following line is writing the depth into the actual depth buffer + return asfloat(uDepth); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader new file mode 100644 index 0000000000..5b518c3bfc --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader @@ -0,0 +1,37 @@ +{ + "Source" : "HairShortCutResolveDepth.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, // test the written depth and accept/discard based on the depth buffer + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : false + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveDepthPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl index 496aa5c7da..ed6ec5de27 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl @@ -31,7 +31,7 @@ // THE SOFTWARE. // //-------------------------------------------------------------------------------------- -#include +#include #include //-------------------------------------------------------------------------------------- diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli index f95dac92cf..3ded3ab2af 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli @@ -29,13 +29,13 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairSimulationComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ #pragma once -#include +#include //!----------------------------------------------------------------------------- //! diff --git a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli index b8c4ea9eb4..abbbc8c0de 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli @@ -66,12 +66,22 @@ float3 GetSharedTangent(int tangentIndex) ); } +//! Hair vertex geometry output - input structure for the Pixel shaders struct TressFXVertex { float4 Position; - float4 Tangent; + float4 Tangent; // xyz = Tangent, w = Strand U float4 p0p1; - float4 StrandColor; + float4 StrandColor; // xyz = Strand Color, w = Strand V +}; + +//! Matching structure to carry out as VS output / PS input +struct PS_INPUT_HAIR +{ + float4 Position : SV_POSITION; + float4 Tangent : Tangent; + float4 p0p1 : TEXCOORD0; + float4 StrandColor : TEXCOORD1; }; float3 GetStrandColor(int index, float fractionOfStrand) @@ -178,5 +188,26 @@ TressFXVertex GetExpandedTressFXShadowVert(uint vertexId, float3 eye, float2 win return Output; } -// EndHLSL +//!============================================================================= +//! Hair Render VS - Used by all geometry hair shaders +//!============================================================================= +PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) +{ + PS_INPUT_HAIR vsOutput; + // uint2 scrSize; + // PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); + // TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); + + // [To Do] Hair: the above code should replace the existing but requires modifications to + // the function GetExpandedTressFXVert. + // Note that in Atom g_vViewport is aspect ratio and NOT size. + TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); + + vsOutput.Position = tressfxVert.Position; + vsOutput.Tangent = tressfxVert.Tangent; + vsOutput.p0p1 = tressfxVert.p0p1; + vsOutput.StrandColor = tressfxVert.StrandColor; + + return vsOutput; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli index 1d50d3c040..f91ba1ff3b 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli @@ -34,9 +34,6 @@ #pragma once -#include - - #define SHORTCUT_MIN_ALPHA 0.02 #define TRESSFX_FLOAT_EPSILON 1e-7 @@ -52,40 +49,6 @@ float4 MatrixMult(float4x4 m, float4 v) return mul(m, v); } -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the NDC. -// screenCoords - from 0.. dimension of the screen of the current pixel -// screenTexture - screen buffer texture representing the same resolution we work in -// sDepth - the depth buffer depth at the fragment location -// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) -float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) -{ - uint2 dimensions; - screenTexture.GetDimensions(dimensions.x, dimensions.y); - float2 UV = saturate(screenCoords / dimensions.xy); - - float x = UV.x * 2.0f - 1.0f; - float y = (1.0f - UV.y) * 2.0f - 1.0f; - float3 NDC = float3(x, y, depth); - - return NDC; -} - -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the world space position -float3 ScreenPosToWorldPos( - Texture2D screenTexture, float2 screenCoords, float depth, - inout float3 screenPosNDC ) -{ - screenPosNDC = ScreenPosToNDC(PassSrg::m_linearDepth, screenCoords, depth); - float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] - float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); - positionVS /= positionVS.w; // notice the normalization factor - crucial! - float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); - - return positionWS.xyz; -} - // Pack a float4 into an uint uint PackFloat4IntoUint(float4 vValue) { diff --git a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp index 66ac3d2e09..2ae8b9d97d 100644 --- a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp +++ b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp @@ -80,8 +80,14 @@ namespace AZ // Load the AtomTressFX pass classes passSystem->AddPassCreator(Name("HairSkinningComputePass"), &HairSkinningComputePass::Create); + + // Load the PPLL render method passes passSystem->AddPassCreator(Name("HairPPLLRasterPass"), &HairPPLLRasterPass::Create); passSystem->AddPassCreator(Name("HairPPLLResolvePass"), &HairPPLLResolvePass::Create); + + // Load the ShortCut render method passes + passSystem->AddPassCreator(Name("HairShortCutGeometryDepthAlphaPass"), &HairShortCutGeometryDepthAlphaPass::Create); + passSystem->AddPassCreator(Name("HairShortCutGeometryShadingPass"), &HairShortCutGeometryShadingPass::Create); } void HairSystemComponent::Deactivate() diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp index 7af5903cf3..b9c0d2e379 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp @@ -165,6 +165,15 @@ namespace AZ return true; } + Data::Instance HairGeometryRasterPass::GetShader() + { + if (!m_initialized || !m_shader) + { + AZ_Error("Hair Gem", LoadShaderAndPipelineState(), "HairGeometryRasterPass could not initialize pipeline or shader"); + } + return m_shader; + } + void HairGeometryRasterPass::SchedulePacketBuild(HairRenderObject* hairObject) { m_newRenderObjects.insert(hairObject); @@ -188,7 +197,7 @@ namespace AZ // The PerPass is gathered through the RasterPass::m_shaderResourceGroup AZStd::lock_guard lock(m_mutex); - return hairObject->BuildPPLLDrawPacket(drawRequest); + return hairObject->BuildDrawPacket(m_shader.get(), drawRequest); } bool HairGeometryRasterPass::AddDrawPackets(AZStd::list>& hairRenderObjects) @@ -205,7 +214,7 @@ namespace AZ for (auto& renderObject : hairRenderObjects) { - const RHI::DrawPacket* drawPacket = renderObject->GetFillDrawPacket(); + const RHI::DrawPacket* drawPacket = renderObject->GetGeometrylDrawPacket(m_shader.get()); if (!drawPacket) { // might not be an error - the object might have just been added and the DrawPacket is // scheduled to be built when the render frame begins diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h index a226d2c294..d3ba22039d 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h @@ -51,7 +51,7 @@ namespace AZ //! The following will be called when an object was added or shader has been compiled void SchedulePacketBuild(HairRenderObject* hairObject); - Data::Instance GetShader() { return m_shader; } + Data::Instance GetShader(); void SetFeatureProcessor(HairFeatureProcessor* featureProcessor) { @@ -76,7 +76,6 @@ namespace AZ // Pass behavior overrides void InitializeInternal() override; -// void BuildInternal() override; void FrameBeginInternal(FramePrepareParams params) override; // Scope producer functions... diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp index fa643a5488..de66d91e0b 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp @@ -30,6 +30,17 @@ namespace AZ HairPPLLResolvePass::HairPPLLResolvePass(const RPI::PassDescriptor& descriptor) : RPI::FullscreenTrianglePass(descriptor) { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); } void HairPPLLResolvePass::UpdateGlobalShaderOptions() @@ -38,17 +49,17 @@ namespace AZ m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); - shaderOption.SetValue(AZ::Name("o_enableShadows"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); - shaderOption.SetValue(AZ::Name("o_enableDirectionalLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); - shaderOption.SetValue(AZ::Name("o_enablePunctualLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); - shaderOption.SetValue(AZ::Name("o_enableAreaLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); - shaderOption.SetValue(AZ::Name("o_enableIBL"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); - shaderOption.SetValue(AZ::Name("o_hairLightingModel"), AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_R"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TRT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); - shaderOption.SetValue(AZ::Name("o_enableLongtitudeCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); - shaderOption.SetValue(AZ::Name("o_enableAzimuthCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); } diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h index bf1a3f768e..036659b9a5 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h @@ -58,9 +58,20 @@ namespace AZ void CompileResources(const RHI::FrameGraphCompileContext& context) override; private: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + HairPPLLResolvePass(const RPI::PassDescriptor& descriptor); - private: void UpdateGlobalShaderOptions(); HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp index 24cd6465dd..1b0b034dba 100644 --- a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace AZ { diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp new file mode 100644 index 0000000000..5e402e33f6 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp @@ -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 + * + */ + +#include +#include +#include + +#include +#include +#include + +namespace AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryDepthAlphaPass::HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + SetShaderPath("Shaders/hairshortcutgeometrydepthalpha.azshader"); + } + + RPI::Ptr HairShortCutGeometryDepthAlphaPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryDepthAlphaPass(descriptor); + return pass; + } + + void HairShortCutGeometryDepthAlphaPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h new file mode 100644 index 0000000000..da6e28fe31 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h @@ -0,0 +1,49 @@ +/* + * 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 + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryDepthAlphaPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryDepthAlphaPass); + + public: + AZ_RTTI(HairShortCutGeometryDepthAlphaPass, "{F09A0411-B1FF-4085-98E7-6B8B0E1B2C3D}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryDepthAlphaPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + explicit HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor); + + // Pass behavior overrides + void BuildInternal() override; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp new file mode 100644 index 0000000000..eda7f9f83a --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp @@ -0,0 +1,111 @@ +/* + * 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 +#include +#include + +#include +#include +#include + +namespace AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryShadingPass::HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); + + SetShaderPath("Shaders/hairshortcutgeometryshading.azshader"); + } + + RPI::Ptr HairShortCutGeometryShadingPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryShadingPass(descriptor); + return pass; + } + + void HairShortCutGeometryShadingPass::UpdateGlobalShaderOptions() + { + RPI::ShaderOptionGroup shaderOption = m_shader->CreateShaderOptionGroup(); + + m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); + + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + + m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); + } + + void HairShortCutGeometryShadingPass::CompileResources(const RHI::FrameGraphCompileContext& context) + { + if (!m_shaderResourceGroup || !AcquireFeatureProcessor()) + { + AZ_Error("Hair Gem", m_shaderResourceGroup, "HairShortCutGeometryShadingPass: missing Srg or no feature processor yet"); + return; // no error message due to FP - initialization not complete yet, wait for the next frame + } + + UpdateGlobalShaderOptions(); + + if (m_shaderResourceGroup->HasShaderVariantKeyFallbackEntry()) + { + m_shaderResourceGroup->SetShaderVariantKeyFallbackValue(m_shaderOptions); + } + + // Update the material array constant buffer within the per pass srg + SrgBufferDescriptor descriptor = SrgBufferDescriptor( + RPI::CommonBufferPoolType::Constant, RHI::Format::Unknown, + sizeof(AMD::TressFXShadeParams), 1, + Name{ "HairMaterialsArray" }, Name{ "m_hairParams" }, 0, 0 + ); + + m_featureProcessor->GetMaterialsArray().UpdateGPUData(m_shaderResourceGroup, descriptor); + + // Compilation of remaining srgs will be done by the parent class + RPI::RasterPass::CompileResources(context); + } + + void HairShortCutGeometryShadingPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h new file mode 100644 index 0000000000..d728803473 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h @@ -0,0 +1,69 @@ +/* + * 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 +#include + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryShadingPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryShadingPass); + + public: + AZ_RTTI(HairShortCutGeometryShadingPass, "{11BA673D-0788-4B25-978D-9737BF4E48FE}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryShadingPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + void CompileResources(const RHI::FrameGraphCompileContext& context) override; + + protected: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + + explicit HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor); + + void UpdateGlobalShaderOptions(); + + // Pass behavior overrides + void BuildInternal() override; + + HairGlobalSettings m_hairGlobalSettings; + AZ::RPI::ShaderVariantKey m_shaderOptions; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 7a317dc09c..b7c6bbce13 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -47,11 +47,11 @@ namespace AZ HairFeatureProcessor::HairFeatureProcessor() { + m_usePPLLRenderTechnique = false; // Use the ShortCut rendering technique + HairParentPassName = Name{ "HairParentPass" }; - HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; - HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; - + // Hair Skinning and Simulation Compute passes GlobalShapeConstraintsPassName = Name{ "HairGlobalShapeConstraintsComputePass" }; CalculateStrandDataPassName = Name{ "HairCalculateStrandLevelDataComputePass" }; VelocityShockPropagationPassName = Name{ "HairVelocityShockPropagationComputePass" }; @@ -59,12 +59,21 @@ namespace AZ LengthConstriantsWindAndCollisionPassName = Name{ "HairLengthConstraintsWindAndCollisionComputePass" }; UpdateFollowHairPassName = Name{ "HairUpdateFollowHairComputePass" }; + // PPLL render technique pases + HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; + HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; + + // ShortCut render technique pases + HairShortCutGeometryDepthAlphaPassName = Name{ "HairShortCutGeometryDepthAlphaPass" }; + HairShortCutResolveDepthPassName = Name{ "HairShortCutResolveDepthPass" }; + HairShortCutGeometryShadingPassName = Name{ "HairShortCutGeometryShadingPass" }; + HairShortCutResolveColorPassName = Name{ "HairShortCutResolveColorPass" }; + ++s_instanceCount; if (!CreatePerPassResources()) { // this might not be an error - if the pass system is still empty / minimal - // and these passes are not part of the minimal pipeline, they will not - // be created. + // and these passes are not part of the minimal pipeline, they will not be created. AZ_Error("Hair Gem", false, "Failed to create the hair shared buffer resource"); } } @@ -127,25 +136,33 @@ namespace AZ m_hairRenderObjects.push_back(renderObject); + // Adding the object will schedule Srgs binding and the DrawItem build for the geometry passes. BuildDispatchAndDrawItems(renderObject); EnablePasses(true); } - void HairFeatureProcessor::EnablePasses(bool enable) + void HairFeatureProcessor::EnablePasses([[maybe_unused]] bool enable) { + return; + + // [To Do] - This part should be enabled (remove the return) to reduce overhead + // when Hair is disabled / doesn't exist in the scene. + // Currently it might break features such as fog that depend on the output and for some + // reason doesn't quite work for ShortCut. + // The current overhead is minimal (< 0.1 msec) and this Gem is disabled by default. +/* if (!m_initialized) { return; } - for (auto& [passName, pass] : m_computePasses) + RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairParentPassName); + if (desiredPass) { - pass->SetEnabled(enable); + desiredPass->SetEnabled(enable); } - - m_hairPPLLRasterPass->SetEnabled(enable); - m_hairPPLLResolvePass->SetEnabled(enable); +*/ } bool HairFeatureProcessor::RemoveHairRenderObject(Data::Instance renderObject) @@ -214,7 +231,8 @@ namespace AZ } if (m_forceRebuildRenderData) - { + { // In the case of a force build, schedule Srgs binding and the DrawItem build for + // the geometry passes of all existing hair objects. for (auto& hairRenderObject : m_hairRenderObjects) { BuildDispatchAndDrawItems(hairRenderObject); @@ -276,17 +294,32 @@ namespace AZ pass->AddDispatchItems(m_hairRenderObjects); } - // Add all hair objects to the Render / Raster Pass - m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + if (m_usePPLLRenderTechnique) + { + // Add all hair objects to the Render / Raster Pass + m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->AddDrawPackets(m_hairRenderObjects); + m_hairShortCutGeometryShadingPass->AddDrawPackets(m_hairRenderObjects); + } } void HairFeatureProcessor::ClearPasses() { m_initialized = false; // Avoid simulation or render m_computePasses.clear(); + + // PPLL geometry and resolve full screen passes m_hairPPLLRasterPass = nullptr; m_hairPPLLResolvePass = nullptr; + // ShortCut passes - Special handling of geometry passes only, and using the regular + // full screen pass for the resolve + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + // Mark for all passes to evacuate their render data and recreate it. m_forceRebuildRenderData = true; m_forceClearRenderData = true; @@ -338,6 +371,12 @@ namespace AZ ClearPasses(); + if (!m_renderPipeline) + { + AZ_Error("Hair Gem", false, "HairFeatureProcessor does NOT have render pipeline set yet"); + return false; + } + // Compute Passes - populate the passes map bool resultSuccess = InitComputePass(GlobalShapeConstraintsPassName); resultSuccess &= InitComputePass(CalculateStrandDataPassName); @@ -347,8 +386,15 @@ namespace AZ resultSuccess &= InitComputePass(UpdateFollowHairPassName); // Rendering Passes - resultSuccess &= InitPPLLFillPass(); - resultSuccess &= InitPPLLResolvePass(); + if (m_usePPLLRenderTechnique) + { + resultSuccess &= InitPPLLFillPass(); + resultSuccess &= InitPPLLResolvePass(); + } + else + { + resultSuccess &= InitShortCutRenderPasses(); + } m_initialized = resultSuccess; @@ -388,7 +434,8 @@ namespace AZ } } - // PPLL nodes buffer + // PPLL nodes buffer - created only if the PPLL technique is used + if (m_usePPLLRenderTechnique) { descriptor = SrgBufferDescriptor( RPI::CommonBufferPoolType::ReadWrite, RHI::Format::Unknown, @@ -425,11 +472,6 @@ namespace AZ bool HairFeatureProcessor::InitComputePass(const Name& passName, bool allowIterations) { m_computePasses[passName] = nullptr; - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "%s does NOT have render pipeline set yet", passName.GetCStr()); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(passName); if (desiredPass) @@ -452,11 +494,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLFillPass() { m_hairPPLLRasterPass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLRasterPassName); if (desiredPass) @@ -466,7 +503,7 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLRasterPass does not have any valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLRasterPass cannot be found. Check your game project's .pass assets."); return false; } return true; @@ -475,11 +512,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLResolvePass() { m_hairPPLLResolvePass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLResolvePassName); if (desiredPass) @@ -489,12 +521,46 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLResolvePassTemplate does not have valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLResolvePass cannot be found. Check your game project's .pass assets."); return false; } return true; } + //! Set the two short cut geometry pases and assign them the FP. The other two full screen passes + //! are generic full screen passes and don't need any interaction with the FP. + bool HairFeatureProcessor::InitShortCutRenderPasses() + { + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + + m_hairShortCutGeometryDepthAlphaPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryDepthAlphaPassName).get()); + if (m_hairShortCutGeometryDepthAlphaPass) + { + m_hairShortCutGeometryDepthAlphaPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutResolveDepthPass cannot be found. Check your game project's .pass assets."); + return false; + } + + m_hairShortCutGeometryShadingPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryShadingPassName).get()); + if (m_hairShortCutGeometryShadingPass) + { + m_hairShortCutGeometryShadingPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutGeometryShadingPass cannot be found. Check your game project's .pass assets."); + return false; + } + + return true; + } + void HairFeatureProcessor::BuildDispatchAndDrawItems(Data::Instance renderObject) { HairRenderObject* renderObjectPtr = renderObject.get(); @@ -513,9 +579,18 @@ namespace AZ m_computePasses[UpdateFollowHairPassName]->BuildDispatchItem( renderObjectPtr, DispatchLevel::DISPATCHLEVEL_VERTEX); - // Render / Raster pass - adding the object will schedule Srgs binding - // and DrawItem build. - m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + // Schedule Srgs binding and the DrawItem build. + // Since this does not bind the PerPass srg but prepare the rest of the Srgs + // such as the dynamic srg, it should only be done once per object per frame. + if (m_usePPLLRenderTechnique) + { + m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->SchedulePacketBuild(renderObjectPtr); + m_hairShortCutGeometryShadingPass->SchedulePacketBuild(renderObjectPtr); + } } Data::Instance HairFeatureProcessor::GetHairSkinningComputegPass() @@ -527,14 +602,28 @@ namespace AZ return m_computePasses[GlobalShapeConstraintsPassName]; } - Data::Instance HairFeatureProcessor::GetHairPPLLRasterPass() + Data::Instance HairFeatureProcessor::GetGeometryRasterShader() { - if (!m_hairPPLLRasterPass) + if (m_usePPLLRenderTechnique) { - Init(m_renderPipeline); + if (!m_hairPPLLRasterPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairPPLLRasterPass was not created"); + return nullptr; + } + return m_hairPPLLRasterPass->GetShader(); } - return m_hairPPLLRasterPass; + + if (!m_hairShortCutGeometryDepthAlphaPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairShortCutGeometryDepthAlphaPass was not created"); + return nullptr; + } + return m_hairShortCutGeometryDepthAlphaPass->GetShader(); } + } // namespace Hair } // namespace Render } // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h index c56dc5c921..46660a6623 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h @@ -17,14 +17,19 @@ #include #include +#include // Hair specific #include #include + #include #include +#include +#include + #include #include #include @@ -73,9 +78,7 @@ namespace AZ { Name HairParentPassName; - Name HairPPLLRasterPassName; - Name HairPPLLResolvePassName; - + // Compute passes Name GlobalShapeConstraintsPassName; Name CalculateStrandDataPassName; Name VelocityShockPropagationPassName; @@ -83,6 +86,16 @@ namespace AZ Name LengthConstriantsWindAndCollisionPassName; Name UpdateFollowHairPassName; + // PPLL render passes + Name HairPPLLRasterPassName; + Name HairPPLLResolvePassName; + + // ShortCut render passes + Name HairShortCutGeometryDepthAlphaPassName; + Name HairShortCutResolveDepthPassName; + Name HairShortCutGeometryShadingPassName; + Name HairShortCutResolveColorPassName; + public: AZ_RTTI(AZ::Render::Hair::HairFeatureProcessor, "{5F9DDA81-B43F-4E30-9E56-C7C3DC517A4C}", RPI::FeatureProcessor); @@ -117,6 +130,7 @@ namespace AZ Data::Instance GetHairSkinningComputegPass(); Data::Instance GetHairPPLLRasterPass(); + Data::Instance GetGeometryRasterShader(); //! Update the hair objects materials array. void FillHairMaterialsArray(std::vector& renderSettings); @@ -144,6 +158,7 @@ namespace AZ bool InitPPLLFillPass(); bool InitPPLLResolvePass(); + bool InitShortCutRenderPasses(); bool InitComputePass(const Name& passName, bool allowIterations = false); void BuildDispatchAndDrawItems(Data::Instance renderObject); @@ -168,10 +183,14 @@ namespace AZ //! Simulation Compute Passes AZStd::unordered_map > m_computePasses; - // Render Passes + // PPLL Render Passes Data::Instance m_hairPPLLRasterPass = nullptr; Data::Instance m_hairPPLLResolvePass = nullptr; + // ShortCut Render Passes - special case for the geometry render passes + Data::Instance m_hairShortCutGeometryDepthAlphaPass = nullptr; + Data::Instance m_hairShortCutGeometryShadingPass = nullptr; + //-------------------------------------------------------------- // Per Pass Resources //-------------------------------------------------------------- @@ -196,6 +215,7 @@ namespace AZ bool m_forceClearRenderData = false; bool m_initialized = false; bool m_isEnabled = true; + bool m_usePPLLRenderTechnique = true; static uint32_t s_instanceCount; HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp index baf986daf6..4d615e31ae 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp @@ -993,7 +993,7 @@ namespace AZ //------------------------------------- // Dynamic buffers, data and Srg creation - shared between passes and changed on the GPU if (!m_dynamicHairData.CreateDynamicGPUResources( - m_skinningShader, m_PPLLFillShader, + m_skinningShader, m_geometryRasterShader, m_NumTotalVertices, m_NumTotalStrands)) { AZ_Error("Hair Gem", false, "Hair - Error creating dynamic resources [%s]", assetName ); @@ -1028,7 +1028,7 @@ namespace AZ // Rendering setup bool renderResourcesSuccess; - renderResourcesSuccess = CreateRenderingGPUResources(m_PPLLFillShader, *asset, assetName); + renderResourcesSuccess = CreateRenderingGPUResources(m_geometryRasterShader, *asset, assetName); renderResourcesSuccess &= PopulateDrawStrandsBindSet(renderSettings); renderResourcesSuccess &= UploadRenderingGPUResources(*asset); @@ -1057,17 +1057,10 @@ namespace AZ } { - Data::Instance rasterPass = m_featureProcessor->GetHairPPLLRasterPass(); - if (!rasterPass.get()) + m_geometryRasterShader = m_featureProcessor->GetGeometryRasterShader(); + if (!m_geometryRasterShader) { - AZ_Error("Hair Gem", false, "Failed to get PPLL raster fill Pass."); - return false; - } - - m_PPLLFillShader = rasterPass->GetShader(); - if (!m_PPLLFillShader) - { - AZ_Error("Hair Gem", false, "Failed to get hair raster fill shader from raster pass"); + AZ_Error("Hair Gem", false, "Failed to get hair geometry raster shader"); return false; } } @@ -1116,7 +1109,7 @@ namespace AZ return updatedCB; } - bool HairRenderObject::BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest) + bool HairRenderObject::BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest) { RHI::DrawPacketBuilder drawPacketBuilder; RHI::DrawIndexed drawIndexed; @@ -1159,21 +1152,38 @@ namespace AZ drawPacketBuilder.AddShaderResourceGroup(simSrg->GetRHIShaderResourceGroup()); drawPacketBuilder.AddDrawItem(drawRequest); - if (m_fillDrawPacket) - { - delete m_fillDrawPacket; - } - m_fillDrawPacket = drawPacketBuilder.End(); - - if (!m_fillDrawPacket) + const RHI::DrawPacket* drawPacket = drawPacketBuilder.End(); + if (!drawPacket) { AZ_Error("Hair Gem", false, "Failed to build the hair DrawPacket."); return false; } + // Insert the newly created draw packet to the map based on its shader + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter != m_geometryDrawPackets.end()) + { + delete iter->second; + iter->second = drawPacket; + } + else + { + m_geometryDrawPackets[geometryShader] = drawPacket; + } + return true; } + const RHI::DrawPacket* HairRenderObject::GetGeometrylDrawPacket(RPI::Shader* geometryShader) + { + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter == m_geometryDrawPackets.end()) + { + return nullptr; + } + return iter->second; + } + const RHI::DispatchItem* HairRenderObject::GetDispatchItem(RPI::Shader* computeShader) { auto dispatchIter = m_dispatchItems.find(computeShader); @@ -1210,4 +1220,3 @@ namespace AZ } // namespace Hair } // namespace Render } // namespace AZ - diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h index fa4095eed0..817ebd89fa 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h @@ -179,14 +179,9 @@ namespace AZ AMD::TressFXSimulationSettings* simSettings, AMD::TressFXRenderingSettings* renderSettings ); - //! Creates and fill the draw item associated with the PPLL render of the - //! current hair object - const RHI::DrawPacket* GetFillDrawPacket() - { - return m_fillDrawPacket; - } + bool BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest); - bool BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest); + const RHI::DrawPacket* GetGeometrylDrawPacket(RPI::Shader* geometryShader); //! Creates and fill the dispatch item associated with the compute shader bool BuildDispatchItem(RPI::Shader* computeShader, DispatchLevel dispatchLevel); @@ -302,17 +297,20 @@ namespace AZ //! responsible for the various stages and passes' updates HairFeatureProcessor* m_featureProcessor = nullptr; - //! The dispatch item used for the skinning - HairDispatchItem m_skinningDispatchItem; - - //! Compute dispatch items map per the existing passes - AZStd::map> m_dispatchItems; - //! Skinning compute shader used for creation of the compute Srgs and dispatch item Data::Instance m_skinningShader = nullptr; - //! PPLL fill shader used for creation of the raster Srgs and draw item - Data::Instance m_PPLLFillShader = nullptr; + //! Compute dispatch items map per the existing passes + AZStd::unordered_map> m_dispatchItems; + + //! Geometry raster shader used for creation of the raster Srgs. + //! Since the Srgs for geometry raster are the same across the shaders we keep + //! only a single shader - if this to change in the future, several shaders and sets + //! of dynamic Srgs should be created. + Data::Instance m_geometryRasterShader = nullptr; + + //! DrawPacket for the multi object geometry raster pass. + AZStd::unordered_map m_geometryDrawPackets; float m_frameDeltaTime = 0.02; @@ -378,9 +376,6 @@ namespace AZ //! Index buffer for the render pass via draw calls - naming was kept Data::Instance m_indexBuffer; RHI::IndexBufferView m_indexBufferView; - - //! DrawPacket for the multi object raster fill pass. - const RHI::DrawPacket* m_fillDrawPacket = nullptr; //------------------------------------------------------------------- AZStd::mutex m_mutex; diff --git a/Gems/AtomTressFX/Hair_files.cmake b/Gems/AtomTressFX/Hair_files.cmake index 000abeb559..180685d499 100644 --- a/Gems/AtomTressFX/Hair_files.cmake +++ b/Gems/AtomTressFX/Hair_files.cmake @@ -67,6 +67,13 @@ set(FILES # Base class of all geometry raster passes Code/Passes/HairGeometryRasterPass.h Code/Passes/HairGeometryRasterPass.cpp + + # ShortCut rendering technique - pass classes + Code/Passes/HairShortCutGeometryDepthAlphaPass.h + Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp + Code/Passes/HairShortCutGeometryShadingPass.h + Code/Passes/HairShortCutGeometryShadingPass.cpp + # PPLL rendering technique - geometry raster pass Code/Passes/HairPPLLRasterPass.h Code/Passes/HairPPLLRasterPass.cpp @@ -84,30 +91,37 @@ set(FILES Code/Assets/HairAsset.cpp #) #set(shaders_sources - # Srgs and Utility files - Assets/Shaders/HairSrgs.azsli - Assets/Shaders/HairSimulationSrgs.azsli + # Geometry and Full Screen azsl utility files Assets/Shaders/HairRenderingSrgs.azsli - Assets/Shaders/HairSimulationCommon.azsli Assets/Shaders/HairStrands.azsli Assets/Shaders/HairUtilities.azsli + Assets/Shaders/HairFullScreenUtils.azsli Assets/Shaders/HairLighting.azsli Assets/Shaders/HairLightingEquations.azsli Assets/Shaders/HairLightTypes.azsli Assets/Shaders/HairSurface.azsli - # Simulation Compute shaders - Assets/Shaders/HairSimulationCompute.azsl - - # Collision shaders - to be included soon -# Assets/Shaders/HairCollisionPrepareSDF.azsl -# Assets/Shaders/HairCollisionWithSDF.azsl + # ShortCut technique shaders (using multiple RTs instead of PPLL for GPU memory reduction) + Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl + Assets/Shaders/HairShortCutResolveDepth.azsl + Assets/Shaders/HairShortCutGeometryShading.azsl + Assets/Shaders/HairShortCutResolveColor.azsl - # Rendering shaders + # Rendering azsl files Assets/Shaders/HairRenderingFillPPLL.azsl Assets/Shaders/HairRenderingResolvePPLL.azsl - # Simulation .shader files + # Simulation Compute azsl files + Assets/Shaders/HairComputeSrgs.azsli + Assets/Shaders/HairSimulationComputeSrgs.azsli + Assets/Shaders/HairSimulationCommon.azsli + Assets/Shaders/HairSimulationCompute.azsl + + # Collision azsl files - to be included soon +# Assets/Shaders/HairCollisionPrepareSDF.azsl +# Assets/Shaders/HairCollisionWithSDF.azsl + + # Simulation Compute .shader files Assets/Shaders/HairGlobalShapeConstraintsCompute.shader Assets/Shaders/HairCalculateStrandLevelDataCompute.shader Assets/Shaders/HairVelocityShockPropagationCompute.shader @@ -115,9 +129,15 @@ set(FILES Assets/Shaders/HairLengthConstraintsWindAndCollisionCompute.shader Assets/Shaders/HairUpdateFollowHairCompute.shader - # Rendering .shader file + # PPLL Render .shader file Assets/Shaders/HairRenderingFillPPLL.shader Assets/Shaders/HairRenderingResolvePPLL.shader + + # ShortCut Render .shader file + Assets/Shaders/HairShortCutGeometryDepthAlpha.shader + Assets/Shaders/HairShortCutResolveDepth.shader + Assets/Shaders/HairShortCutGeometryShading.shader + Assets/Shaders/HairShortCutResolveColor.shader # Colisions .shader files - to be included soon # Assets/Shaders/HairCollisionInitializeSDF.shader @@ -127,15 +147,25 @@ set(FILES #) # #set(atom_hair_passes + # Compute simulation and skinning passes Assets/Passes/HairParentPass.pass + Assets/Passes/HairParentShortCutPass.pass Assets/Passes/HairGlobalShapeConstraintsCompute.pass Assets/Passes/HairCalculateStrandLevelDataCompute.pass Assets/Passes/HairVelocityShockPropagationCompute.pass Assets/Passes/HairLocalShapeConstraintsCompute.pass Assets/Passes/HairLengthConstraintsWindAndCollisionCompute.pass Assets/Passes/HairUpdateFollowHairCompute.pass + + # PPLL render passes Assets/Passes/HairFillPPLL.pass Assets/Passes/HairResolvePPLL.pass + + # Shortcut render passes + Assets/Passes/HairShortCutGeometryDepthAlpha.pass + Assets/Passes/HairShortCutResolveDepth.pass + Assets/Passes/HairShortCutGeometryShading.pass + Assets/Passes/HairShortCutResolveColor.pass ) set(SKIP_UNITY_BUILD_INCLUSION_FILES diff --git a/Gems/AudioEngineWwise/Code/CMakeLists.txt b/Gems/AudioEngineWwise/Code/CMakeLists.txt index 98a31fb91a..33c80bc31c 100644 --- a/Gems/AudioEngineWwise/Code/CMakeLists.txt +++ b/Gems/AudioEngineWwise/Code/CMakeLists.txt @@ -18,31 +18,18 @@ set(AUDIOENGINEWWISE_COMPILEDEFINITIONS find_package(Wwise MODULE) ################################################################################ -# Server / Unsupported +# Servers +# (and situations where Wwise SDK is not found or otherwise unavailable) ################################################################################ -if (PAL_TRAIT_BUILD_SERVER_SUPPORTED OR PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # Stub gem for server and unsupported platforms. Audio Engine Wwise is client only - ly_add_target( - NAME AudioEngineWwise.Stub ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} - NAMESPACE Gem - FILES_CMAKE - audioenginewwise_stub_files.cmake - BUILD_DEPENDENCIES - PRIVATE - AZ::AzCore - ) -endif() - -if (PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # setup aliases so stubs will be used if something references AudioEngineWwise(.Editor) - add_library(Gem::AudioEngineWwise ALIAS AudioEngineWwise.Stub) - add_library(Gem::AudioEngineWwise.Editor ALIAS AudioEngineWwise.Stub) +if(NOT PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED OR NOT Wwise_FOUND) + # Don't create any Gem targets and aliases. Nothing should depend on this + # Gem directly, because if it doesn't define targets it will cause an error. return() endif() ################################################################################ -# Runtime / Game +# Clients ################################################################################ ly_add_target( NAME AudioEngineWwise.Static STATIC @@ -181,7 +168,7 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) endif() ################################################################################ -# Tools / Editor +# Tools / Builders ################################################################################ if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp index 38cfee2c26..f7a3d4d69d 100644 --- a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp +++ b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp @@ -46,4 +46,4 @@ namespace Profiler }; }// namespace Profiler -AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerImGuiModule) +AZ_DECLARE_MODULE_CLASS(Gem_ProfilerImGui, Profiler::ProfilerImGuiModule) diff --git a/scripts/o3de/o3de/engine_template.py b/scripts/o3de/o3de/engine_template.py index cba7539aaa..e93f972301 100755 --- a/scripts/o3de/o3de/engine_template.py +++ b/scripts/o3de/o3de/engine_template.py @@ -407,15 +407,12 @@ def create_template(source_path: pathlib.Path, source_name = os.path.basename(source_path) sanitized_source_name = utils.sanitize_identifier_for_cpp(source_name) - # if no template path, error + # if no template path, use default_templates_folder path if not template_path: - logger.info(f'Template path empty. Using source name {source_name}') - template_path = pathlib.Path(source_name) - if not template_path.is_absolute(): default_templates_folder = manifest.get_registered(default_folder='templates') - template_path = default_templates_folder / template_path - logger.info(f'Template path not a full path. Using default templates folder {template_path}') - if not force and template_path.is_dir(): + template_path = default_templates_folder / source_name + logger.info(f'Template path empty. Using default templates folder {template_path}') + if not force and template_path.is_dir() and len(list(template_path.iterdir())): logger.error(f'Template path {template_path} already exists.') return 1 @@ -1105,7 +1102,7 @@ def create_from_template(destination_path: pathlib.Path, logger.error(f'Could not find the template {template_name}=>{template_path}') return 1 - # the template.json should be in the template_path, make sure it's there a nd valid + # the template.json should be in the template_path, make sure it is valid template_json = template_path / 'template.json' if not validation.valid_o3de_template_json(template_json): logger.error(f'Template json {template_path} is invalid.') @@ -1254,7 +1251,7 @@ def create_from_template(destination_path: pathlib.Path, # destination restricted path elif destination_restricted_path: if os.path.isabs(destination_restricted_path): - restricted_default_path = manifest.get_registered(default='restricted') + restricted_default_path = manifest.get_registered(default_folder='restricted') new_destination_restricted_path = restricted_default_path / destination_restricted_path logger.info(f'{destination_restricted_path} is not a full path, making it relative' f' to default restricted path = {new_destination_restricted_path}') @@ -1346,7 +1343,7 @@ def create_project(project_path: pathlib.Path, Template instantiation specialization that makes all default assumptions for a Project template instantiation, reducing the effort needed in instancing a project :param project_path: the project path, can be absolute or relative to default projects path - :param project_name: the project name, defaults to project_path basename if not provided + :param project_name: the project name, defaults to project_path basename if not provided :param template_path: the path to the template you want to instance, can be absolute or relative to default templates path :param template_name: the name the registered template you want to instance, defaults to DefaultProject, resolves template_path :param project_restricted_path: path to the projects restricted folder, can be absolute or relative to the restricted='projects' @@ -1523,13 +1520,9 @@ def create_project(project_path: pathlib.Path, if not project_path: logger.error('Project path cannot be empty.') return 1 - if not os.path.isabs(project_path): - default_projects_folder = manifest.get_registered(default_folder='projects') - new_project_path = default_projects_folder / project_path - logger.info(f'Project Path {project_path} is not a full path, we must assume its relative' - f' to default projects path = {new_project_path}') - project_path = new_project_path - if not force and os.path.isdir(project_path) and len(os.listdir(project_path)) > 0: + + project_path = project_path.resolve() + if not force and project_path.is_dir() and len(list(project_path.iterdir())): logger.error(f'Project path {project_path} already exists and is not empty.') return 1 elif not os.path.isdir(project_path): @@ -1904,14 +1897,10 @@ def create_gem(gem_path: pathlib.Path, if not gem_path: logger.error('Gem path cannot be empty.') return 1 - if not os.path.isabs(gem_path): - default_gems_folder = manifest.get_registered(default_folder='gems') - new_gem_path = default_gems_folder / gem_path - logger.info(f'Gem Path {gem_path} is not a full path, we must assume its relative' - f' to default gems path = {new_gem_path}') - gem_path = new_gem_path - if not force and os.path.isdir(gem_path): - logger.error(f'Gem path {gem_path} already exists.') + + gem_path = gem_path.resolve() + if not force and gem_path.is_dir() and len(list(gem_path.iterdir())): + logger.error(f'Gem path {gem_path} already exists and is not empty.') return 1 else: os.makedirs(gem_path, exist_ok=force) @@ -1936,16 +1925,18 @@ def create_gem(gem_path: pathlib.Path, # gem restricted path elif gem_restricted_path: if not os.path.isabs(gem_restricted_path): - default_gems_restricted_folder = manifest.get_registered(restricted_name='gems') - new_gem_restricted_path = default_gems_restricted_folder /gem_restricted_path - logger.info(f'Gem restricted path {gem_restricted_path} is not a full path, we must assume its' - f' relative to default gems restricted path = {new_gem_restricted_path}') - gem_restricted_path = new_gem_restricted_path - elif template_restricted_path: + gem_restricted_default_path = manifest.get_registered(restricted_name='gems') + if gem_restricted_default_path: + new_gem_restricted_path = gem_restricted_default_path / gem_restricted_path + logger.info(f'Gem restricted path {gem_restricted_path} is not a full path, we must assume its' + f' relative to default gems restricted path = {new_gem_restricted_path}') + gem_restricted_path = new_gem_restricted_path + else: gem_restricted_default_path = manifest.get_registered(restricted_name='gems') - logger.info(f'--gem-restricted-path is not specified, using default gem restricted path / gem name' - f' = {gem_restricted_default_path}') - gem_restricted_path = gem_restricted_default_path + if gem_restricted_default_path: + logger.info(f'--gem-restricted-path is not specified, using default / ' + f' = {gem_restricted_default_path}') + gem_restricted_path = gem_restricted_default_path / gem_name # gem restricted relative if not gem_restricted_platform_relative_path: @@ -1964,7 +1955,7 @@ def create_gem(gem_path: pathlib.Path, replacements.append(("${NameUpper}", gem_name.upper())) replacements.append(("${NameLower}", gem_name.lower())) replacements.append(("${SanitizedCppName}", sanitized_cpp_name)) - + # module id is a uuid with { and - if module_id: @@ -2244,14 +2235,14 @@ def add_args(subparsers) -> None: create_from_template_subparser = subparsers.add_parser('create-from-template') create_from_template_subparser.add_argument('-dp', '--destination-path', type=pathlib.Path, required=True, help='The path to where you want the template instantiated,' - ' can be absolute or dev root relative.' + ' can be absolute or relative to the current working directory.' 'Ex. C:/o3de/Test' 'Test = ') group = create_from_template_subparser.add_mutually_exclusive_group(required=True) group.add_argument('-tp', '--template-path', type=pathlib.Path, required=False, help='The path to the template you want to instantiate, can be absolute' - ' or dev root/Templates relative.' + ' or relative to the current working directory.' 'Ex. C:/o3de/Template/TestTemplate' 'TestTemplate = ') group.add_argument('-tn', '--template-name', type=str, required=False, @@ -2327,7 +2318,7 @@ def add_args(subparsers) -> None: create_project_subparser = subparsers.add_parser('create-project') create_project_subparser.add_argument('-pp', '--project-path', type=pathlib.Path, required=True, help='The location of the project you wish to create from the template,' - ' can be an absolute path or dev root relative.' + ' can be an absolute path or relative to the current working directory.' ' Ex. C:/o3de/TestProject' ' TestProject = if --project-name not provided') create_project_subparser.add_argument('-pn', '--project-name', type=str, required=False, @@ -2349,8 +2340,8 @@ def add_args(subparsers) -> None: group = create_project_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-prp', '--project-restricted-path', type=pathlib.Path, required=False, default=None, - help='path to the projects restricted folder, can be absolute or relative' - ' to the restricted="projects"') + help='path to the projects restricted folder, can be absolute or relative to' + ' the default restricted projects directory') group.add_argument('-prn', '--project-restricted-name', type=str, required=False, default=None, help='The name of the registered projects restricted path. If supplied this will resolve' @@ -2360,7 +2351,7 @@ def add_args(subparsers) -> None: group.add_argument('-trp', '--template-restricted-path', type=pathlib.Path, required=False, default=None, help='The templates restricted path can be absolute or relative to' - ' restricted="templates"') + 'the default restricted templates directory') group.add_argument('-trn', '--template-restricted-name', type=str, required=False, default=None, help='The name of the registered templates restricted path. If supplied this will resolve' @@ -2423,7 +2414,7 @@ def add_args(subparsers) -> None: # creation of a gem from a template (like create from template but makes gem assumptions) create_gem_subparser = subparsers.add_parser('create-gem') create_gem_subparser.add_argument('-gp', '--gem-path', type=pathlib.Path, required=True, - help='The gem path, can be absolute or relative to default gems path') + help='The gem path, can be absolute or relative to the current working directory') create_gem_subparser.add_argument('-gn', '--gem-name', type=str, help='The name to use when substituting the ${Name} placeholder for the gem,' ' must be alphanumeric, ' @@ -2444,19 +2435,18 @@ def add_args(subparsers) -> None: group = create_gem_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-grp', '--gem-restricted-path', type=pathlib.Path, required=False, default=None, - help='The path to the gem restricted to write to folder if any, can be' - 'absolute or dev root relative, default is dev root/restricted.') + help='The gem restricted path, can be absolute or relative to' + ' the default restricted gems directory') group.add_argument('-grn', '--gem-restricted-name', type=str, required=False, default=None, - help='The path to the gem restricted to write to folder if any, can be' - 'absolute or dev root relative, default is dev root/restricted. If supplied' - ' this will resolve the --gem-restricted-path.') + help='The name of the gem to look up the gem restricted path if any.' + 'If supplied this will resolve the --gem-restricted-path.') group = create_gem_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-trp', '--template-restricted-path', type=pathlib.Path, required=False, default=None, help='The templates restricted path, can be absolute or relative to' - ' the restricted="templates"') + ' the default restricted templates directory') group.add_argument('-trn', '--template-restricted-name', type=str, required=False, default=None, help='The name of the registered templates restricted path. If supplied' diff --git a/scripts/o3de/o3de/repo.py b/scripts/o3de/o3de/repo.py index 191fdbd3e1..5c47c2bee8 100644 --- a/scripts/o3de/o3de/repo.py +++ b/scripts/o3de/o3de/repo.py @@ -92,6 +92,44 @@ def process_add_o3de_repo(file_name: str or pathlib.Path, return 0 +def get_gem_json_paths_from_cached_repo(repo_uri: str) -> list: + url = f'{repo_uri}/repo.json' + repo_sha256 = hashlib.sha256(url.encode()) + cache_folder = manifest.get_o3de_cache_folder() + cache_filename = cache_folder / str(repo_sha256.hexdigest() + '.json') + + gem_list = [] + + file_name = pathlib.Path(cache_filename).resolve() + if not file_name.is_file(): + logger.error(f'Could not find cached repo json file for {repo_uri}') + return gem_list + + with file_name.open('r') as f: + try: + repo_data = json.load(f) + except json.JSONDecodeError as e: + logger.error(f'{file_name} failed to load: {str(e)}') + return gem_list + + # Get list of gems, then add all json paths to the list if they exist in the cache + repo_gems = [] + try: + repo_gems.append((repo_data['gems'], 'gem.json')) + except KeyError: + pass + + for o3de_object_uris, manifest_json in repo_gems: + for o3de_object_uri in o3de_object_uris: + manifest_json_uri = f'{o3de_object_uri}/{manifest_json}' + manifest_json_sha256 = hashlib.sha256(manifest_json_uri.encode()) + cache_gem_json_filepath = cache_folder / str(manifest_json_sha256.hexdigest() + '.json') + if cache_gem_json_filepath.is_file(): + logger.warn(f'Could not find cached gem json file for {o3de_object_uri} in repo {repo_uri}') + gem_list.append(cache_gem_json_filepath) + + return gem_list + def refresh_repos() -> int: json_data = manifest.load_o3de_manifest()