diff --git a/AutomatedTesting/Gem/Code/enabled_gems.cmake b/AutomatedTesting/Gem/Code/enabled_gems.cmake index 30740a489d..f653a54505 100644 --- a/AutomatedTesting/Gem/Code/enabled_gems.cmake +++ b/AutomatedTesting/Gem/Code/enabled_gems.cmake @@ -54,4 +54,5 @@ set(ENABLED_GEMS AWSMetrics PrefabBuilder AudioSystem + Profiler ) diff --git a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp index 1528e09169..ae19cab79e 100644 --- a/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp +++ b/AutomatedTesting/Gem/PythonCoverage/Code/Source/PythonCoverageEditorSystemComponent.cpp @@ -204,7 +204,7 @@ namespace PythonCoverage return coveringModuleOutputNames; } - void PythonCoverageEditorSystemComponent::OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) + void PythonCoverageEditorSystemComponent::OnStartExecuteByFilenameAsTest([[maybe_unused]]AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector& args) { if (m_coverageState == CoverageState::Disabled) { @@ -226,8 +226,7 @@ namespace PythonCoverage return; } - const AZStd::string scriptName = AZ::IO::Path(filename).Stem().Native(); - const auto coverageFile = m_coverageDir / AZStd::string::format("%s.pycoverage", scriptName.c_str()); + const auto coverageFile = m_coverageDir / AZStd::string::format("%.*s.pycoverage", AZ_STRING_ARG(testCase)); // If this is a different python script we clear the existing entity components and start afresh if (m_coverageFile != coverageFile) diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py index 783f71e06c..844f8de903 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py @@ -122,17 +122,32 @@ class EditorEntity: # Creation functions @classmethod - def find_editor_entity(cls, entity_name: str) -> EditorEntity: + def find_editor_entity(cls, entity_name: str, must_be_unique : bool = False) -> EditorEntity: """ Given Entity name, outputs entity object :param entity_name: Name of entity to find :return: EditorEntity class object """ - entity_id = general.find_editor_entity(entity_name) - assert entity_id.IsValid(), f"Failure: Couldn't find entity with name: '{entity_name}'" - entity = cls(entity_id) + entities = cls.find_editor_entities([entity_name]) + assert len(entities) != 0, f"Failure: Couldn't find entity with name: '{entity_name}'" + if must_be_unique: + assert len(entities) == 1, f"Failure: Multiple entities with name: '{entity_name}' when expected only one" + + entity = cls(entities[0]) return entity + @classmethod + def find_editor_entities(cls, entity_names: List[str]) -> EditorEntity: + """ + Given Entities names, returns a list of EditorEntity + :param entity_name: Name of entity to find + :return: List[EditorEntity] class object + """ + searchFilter = azlmbr.entity.SearchFilter() + searchFilter.names = entity_names + ids = azlmbr.entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) + return [cls(id) for id in ids] + @classmethod def create_editor_entity(cls, name: str = None, parent_id=None) -> EditorEntity: """ @@ -157,8 +172,7 @@ class EditorEntity: cls, entity_position: Union[List, Tuple, math.Vector3], name: str = None, - parent_id: azlmbr.entity.EntityId = None, - ) -> EditorEntity: + parent_id: azlmbr.entity.EntityId = None) -> EditorEntity: """ Used to create entity at position using 'CreateNewEntityAtPosition' Bus. :param entity_position: World Position(X, Y, Z) of entity in viewport. @@ -227,6 +241,12 @@ class EditorEntity: """ return editor.EditorEntityInfoRequestBus(bus.Event, "GetChildren", self.id) + def get_children(self) -> List[EditorEntity]: + """ + :return: List of EditorEntity children. Type: [EditorEntity] + """ + return [EditorEntity(child_id) for child_id in self.get_children_ids()] + def add_component(self, component_name: str) -> EditorComponent: """ Used to add new component to Entity. diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py index 54bc118f48..510d1b1149 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py @@ -51,7 +51,7 @@ def launch_and_validate_results(request, test_directory, editor, editor_script, logger.debug("Running automated test: {}".format(editor_script)) editor.args.extend(["--skipWelcomeScreenDialog", "--regset=/Amazon/Settings/EnableSourceControl=false", "--regset=/Amazon/Preferences/EnablePrefabSystem=false", run_python, test_case, - f"--pythontestcase={request.node.originalname}", "--runpythonargs", " ".join(cfg_args)]) + f"--pythontestcase={request.node.name}", "--runpythonargs", " ".join(cfg_args)]) if auto_test_mode: editor.args.extend(["--autotest_mode"]) if null_renderer: diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py similarity index 85% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab.py rename to AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py index 9b4d2d1393..2d8b124125 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py @@ -12,20 +12,39 @@ from os import path from PySide2 import QtWidgets +import azlmbr.legacy.general as general from azlmbr.entity import EntityId from azlmbr.math import Vector3 from editor_python_test_tools.editor_entity_utils import EditorEntity from editor_python_test_tools.utils import Report +import azlmbr.entity as entity import azlmbr.bus as bus import azlmbr.prefab as prefab import editor_python_test_tools.pyside_utils as pyside_utils -import prefab.Prefab_Test_Utils as prefab_test_utils + + +def get_prefab_file_path(prefab_path): + if not path.isabs(prefab_path): + prefab_path = path.join(general.get_file_alias("@projectroot@"), prefab_path) + + # Append prefab if it doesn't contain .prefab on it + name, ext = path.splitext(prefab_path) + if ext != ".prefab": + prefab_path = name + ".prefab" + return prefab_path + + +def get_all_entity_ids(): + return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter()) + +def wait_for_propagation(): + general.idle_wait_frames(1) # This is a helper class which contains some of the useful information about a prefab instance. class PrefabInstance: - def __init__(self, prefab_file_name: str=None, container_entity: EditorEntity=EntityId()): + def __init__(self, prefab_file_name: str = None, container_entity: EditorEntity = None): self.prefab_file_name: str = prefab_file_name self.container_entity: EditorEntity = container_entity @@ -42,7 +61,7 @@ class PrefabInstance: 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() -> bool: + def is_valid(self) -> bool: return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs """ @@ -60,7 +79,7 @@ class PrefabInstance: new_parent_before_reparent_children_ids = set(new_parent.get_children_ids()) pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) - pyside_utils.run_soon(lambda: prefab_test_utils.wait_for_propagation()) + pyside_utils.run_soon(lambda: wait_for_propagation()) try: active_modal_widget = await pyside_utils.wait_for_modal_widget() @@ -94,19 +113,18 @@ class Prefab: existing_prefabs = {} - def __init__(self, file_name: str): - self.file_name:str = file_name - self.file_path: str = prefab_test_utils.get_prefab_file_path(file_name) + def __init__(self, file_path: str): + 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_name: A unique file name of the target prefab. + :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_name: str) -> bool: - return file_name in Prefab.existing_prefabs + def is_prefab_loaded(cls, file_path: str) -> bool: + return file_path in Prefab.existing_prefabs """ Check if a prefab exists in the directory for files of prefab tests. @@ -114,9 +132,8 @@ class Prefab: :return: Whether the target prefab exists or not. """ @classmethod - def prefab_exists(cls, file_name: str) -> bool: - file_path = prefab_test_utils.get_prefab_file_path(file_name) - return path.exists(file_path) + def prefab_exists(cls, file_path: str) -> bool: + return path.exists(get_prefab_file_path(file_path)) """ Return a prefab which can be used immediately. @@ -125,10 +142,11 @@ class Prefab: """ @classmethod def get_prefab(cls, file_name: str) -> Prefab: + assert file_name, "Received an empty file_name" if Prefab.is_prefab_loaded(file_name): return Prefab.existing_prefabs[file_name] else: - assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab {file_name} that doesn't exist" + assert Prefab.prefab_exists(file_name), f"Attempted to get a prefab \"{file_name}\" that doesn't exist" new_prefab = Prefab(file_name) Prefab.existing_prefabs[file_name] = Prefab(file_name) return new_prefab @@ -141,7 +159,7 @@ class Prefab: :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) -> (Prefab, PrefabInstance): + def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance): 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) @@ -155,7 +173,7 @@ class Prefab: if prefab_instance_name: container_entity.set_name(prefab_instance_name) - prefab_test_utils.wait_for_propagation() + wait_for_propagation() new_prefab_instance = PrefabInstance(file_name, EditorEntity(container_entity_id)) new_prefab.instances.add(new_prefab_instance) @@ -182,12 +200,13 @@ class Prefab: delete_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DeleteEntitiesAndAllDescendantsInInstance', container_entity_ids) assert delete_prefab_result.IsSuccess(), f"Prefab operation 'DeleteEntitiesAndAllDescendantsInInstance' failed. Error: {delete_prefab_result.GetError()}" - prefab_test_utils.wait_for_propagation() + wait_for_propagation() - entity_ids_after_delete = set(prefab_test_utils.get_all_entities()) + entity_ids_after_delete = set(get_all_entity_ids()) + for entity_id_removed in entity_ids_to_remove: if entity_id_removed in entity_ids_after_delete: - assert prefab_entities_deleted, "Not all entities and descendants in target prefabs are deleted." + assert False, "Not all entities and descendants in target prefabs are deleted." for instance in prefab_instances: instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) @@ -215,12 +234,10 @@ class Prefab: if name: container_entity.set_name(name) - prefab_test_utils.wait_for_propagation() + wait_for_propagation() - new_prefab_instance = PrefabInstance(self.file_name, EditorEntity(container_entity_id)) + new_prefab_instance = PrefabInstance(self.file_path, EditorEntity(container_entity_id)) assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." self.instances.add(new_prefab_instance) - prefab_test_utils.check_entity_at_position(container_entity_id, prefab_position) - return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/Physics/tests/force_region/ForceRegion_LinearDampingForceOnRigidBodies.py b/AutomatedTesting/Gem/PythonTests/Physics/tests/force_region/ForceRegion_LinearDampingForceOnRigidBodies.py index 4e3cc4e43d..f5aae42340 100644 --- a/AutomatedTesting/Gem/PythonTests/Physics/tests/force_region/ForceRegion_LinearDampingForceOnRigidBodies.py +++ b/AutomatedTesting/Gem/PythonTests/Physics/tests/force_region/ForceRegion_LinearDampingForceOnRigidBodies.py @@ -126,7 +126,7 @@ def ForceRegion_LinearDampingForceOnRigidBodies(): # Constants CLOSE_ENOUGH = 0.001 - TIME_OUT = 3.0 + TIME_OUT = 10.0 INITIAL_VELOCITY = azmath.Vector3(0.0, 0.0, -10.0) # 1) Open level / Enter game mode diff --git a/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py b/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py index f5cd66cbbe..cbb6102a44 100755 --- a/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py +++ b/AutomatedTesting/Gem/PythonTests/automatedtesting_shared/base.py @@ -90,7 +90,7 @@ class TestAutomationBase: editor_starttime = time.time() self.logger.debug("Running automated test") testcase_module_filepath = self._get_testcase_module_filepath(testcase_module) - pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={request.node.originalname}"] + pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={request.node.name}"] if use_null_renderer: pycmd += ["-rhi=null"] if batch_mode: diff --git a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py index 09df962b0b..30a0055d5a 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/TestSuite_Main.py @@ -29,21 +29,21 @@ class TestAutomation(TestAutomationBase): autotest_mode=autotest_mode) def test_PrefabLevel_OpensLevelWithEntities(self, request, workspace, editor, launcher_platform): - from . import PrefabLevel_OpensLevelWithEntities as test_module + from .tests import PrefabLevel_OpensLevelWithEntities as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreatePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreatePrefab as test_module + def test_PrefabBasicWorkflow_CreatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_InstantiatePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_InstantiatePrefab as test_module + def test_PrefabBasicWorkflow_InstantiatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_InstantiatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreateAndDeletePrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreateAndDeletePrefab as test_module + def test_PrefabBasicWorkflow_CreateAndDeletePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndDeletePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) - def test_Prefab_BasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): - from . import Prefab_BasicWorkflow_CreateAndReparentPrefab as test_module + 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) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Test.prefab b/AutomatedTesting/Gem/PythonTests/prefab/data/Test.prefab similarity index 100% rename from AutomatedTesting/Gem/PythonTests/prefab/Test.prefab rename to AutomatedTesting/Gem/PythonTests/prefab/data/Test.prefab diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py similarity index 70% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py index f3fbcfa6ad..9ae4614d80 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndDeletePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py @@ -5,14 +5,14 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreateAndDeletePrefab(): +def PrefabBasicWorkflow_CreateAndDeletePrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' from editor_python_test_tools.editor_entity_utils import EditorEntity - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() @@ -21,13 +21,13 @@ def Prefab_BasicWorkflow_CreateAndDeletePrefab(): car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Asserts if prefab creation doesn't succeeds _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Checks for prefab deletion passed or not + # Asserts if prefab deletion fails Prefab.remove_prefabs([car]) if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreateAndDeletePrefab) + Report.start_test(PrefabBasicWorkflow_CreateAndDeletePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py similarity index 85% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py index e5a9d9930a..f78bbf483d 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreateAndReparentPrefab.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py @@ -5,7 +5,7 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreateAndReparentPrefab(): +def PrefabBasicWorkflow_CreateAndReparentPrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' @@ -16,9 +16,9 @@ def Prefab_BasicWorkflow_CreateAndReparentPrefab(): async def run_test(): from editor_python_test_tools.editor_entity_utils import EditorEntity - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() @@ -46,4 +46,4 @@ def Prefab_BasicWorkflow_CreateAndReparentPrefab(): if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreateAndReparentPrefab) + Report.start_test(PrefabBasicWorkflow_CreateAndReparentPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreatePrefab.py similarity index 79% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreatePrefab.py index d86b03d5ad..568a2c15b4 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_CreatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_CreatePrefab.py @@ -5,15 +5,15 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_CreatePrefab(): +def PrefabBasicWorkflow_CreatePrefab(): CAR_PREFAB_FILE_NAME = 'car_prefab' from editor_python_test_tools.editor_entity_utils import EditorEntity from editor_python_test_tools.utils import Report - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() @@ -27,4 +27,4 @@ def Prefab_BasicWorkflow_CreatePrefab(): if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_CreatePrefab) + Report.start_test(PrefabBasicWorkflow_CreatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py similarity index 74% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py index 46be669697..1b962d2ca7 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_BasicWorkflow_InstantiatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py @@ -5,17 +5,17 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -def Prefab_BasicWorkflow_InstantiatePrefab(): +def PrefabBasicWorkflow_InstantiatePrefab(): from azlmbr.math import Vector3 - EXISTING_TEST_PREFAB_FILE_NAME = "Test" + EXISTING_TEST_PREFAB_FILE_NAME = "Gem/PythonTests/Prefab/data/Test.prefab" INSTANTIATED_TEST_PREFAB_POSITION = Vector3(10.00, 20.0, 30.0) EXPECTED_TEST_PREFAB_CHILDREN_COUNT = 1 - from prefab.Prefab import Prefab + from editor_python_test_tools.prefab_utils import Prefab - import prefab.Prefab_Test_Utils as prefab_test_utils + import PrefabTestUtils as prefab_test_utils prefab_test_utils.open_base_tests_level() @@ -31,4 +31,4 @@ def Prefab_BasicWorkflow_InstantiatePrefab(): if __name__ == "__main__": from editor_python_test_tools.utils import Report - Report.start_test(Prefab_BasicWorkflow_InstantiatePrefab) + Report.start_test(PrefabBasicWorkflow_InstantiatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabLevel_OpensLevelWithEntities.py similarity index 87% rename from AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabLevel_OpensLevelWithEntities.py index bc95596845..0eb7e86a9e 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/PrefabLevel_OpensLevelWithEntities.py +++ b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabLevel_OpensLevelWithEntities.py @@ -7,9 +7,9 @@ SPDX-License-Identifier: Apache-2.0 OR MIT # fmt:off class Tests(): - find_empty_entity = ("Entity: 'EmptyEntity' found", "Entity: 'EmptyEntity' *not* found in level") - empty_entity_pos = ("'EmptyEntity' position is at the expected position", "'EmptyEntity' position is *not* at the expected position") - find_pxentity = ("Entity: 'EntityWithPxCollider' found", "Entity: 'EntityWithPxCollider' *not* found in level") + find_empty_entity = ("Entity: 'EmptyEntity' found", "Entity: 'EmptyEntity' *not* found in level") + empty_entity_pos = ("'EmptyEntity' position is at the expected position", "'EmptyEntity' position is *not* at the expected position") + find_pxentity = ("Entity: 'EntityWithPxCollider' found", "Entity: 'EntityWithPxCollider' *not* found in level") pxentity_component = ("Entity: 'EntityWithPxCollider' has a Physx Collider", "Entity: 'EntityWithPxCollider' does *not* have a Physx Collider") # fmt:on diff --git a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py b/AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabTestUtils.py similarity index 81% rename from AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.py rename to AutomatedTesting/Gem/PythonTests/prefab/tests/PrefabTestUtils.py index 3e19911449..f82af23023 100644 --- a/AutomatedTesting/Gem/PythonTests/prefab/Prefab_Test_Utils.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 get_prefab_file_name(prefab_name): - return prefab_name + ".prefab" - -def get_prefab_file_path(prefab_name): - return os.path.join(os.path.dirname(os.path.abspath(__file__)), get_prefab_file_name(prefab_name)) - -def find_entities_by_name(entity_name): - searchFilter = entity.SearchFilter() - searchFilter.names = [entity_name] - return entity.SearchBus(bus.Broadcast, 'SearchEntities', searchFilter) - -def get_all_entities(): - return entity.SearchBus(bus.Broadcast, 'SearchEntities', entity.SearchFilter()) - def check_entity_at_position(entity_id, expected_entity_position): entity_at_expected_position_result = ( "entity is at expected position", @@ -74,9 +60,6 @@ def get_children_ids_by_name(entity_id, entity_name): return result -def wait_for_propagation(): - general.idle_wait_frames(1) - def open_base_tests_level(): helper.init_idle() helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Gem/PythonTests/smoke/test_RemoteConsole_CPULoadLevel_Works.py b/AutomatedTesting/Gem/PythonTests/smoke/test_RemoteConsole_CPULoadLevel_Works.py deleted file mode 100644 index 6522514f2f..0000000000 --- a/AutomatedTesting/Gem/PythonTests/smoke/test_RemoteConsole_CPULoadLevel_Works.py +++ /dev/null @@ -1,44 +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 - - -UI Apps: AutomatedTesting.GameLauncher -Launch AutomatedTesting.GameLauncher with Simple level -Test should run in both gpu and non gpu -""" - -import pytest -import psutil - -import ly_test_tools.environment.waiter as waiter -import editor_python_test_tools.hydra_test_utils as editor_test_utils -from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole -from ly_remote_console.remote_console_commands import ( - send_command_and_expect_response as send_command_and_expect_response, -) - - -@pytest.mark.parametrize("launcher_platform", ["windows"]) -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["Simple"]) -@pytest.mark.SUITE_smoke -class TestRemoteConsoleLoadLevelWorks(object): - @pytest.fixture - def remote_console_instance(self, request): - console = RemoteConsole() - - def teardown(): - if console.connected: - console.stop() - - request.addfinalizer(teardown) - - return console - - def test_RemoteConsole_LoadLevel_Works(self, launcher, level, remote_console_instance, launcher_platform): - expected_lines = ['Level system is loading "Simple"'] - - editor_test_utils.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, null_renderer=True) diff --git a/AutomatedTesting/Levels/Base/Base.ly b/AutomatedTesting/Levels/Base/Base.ly index 8ce19923c0..0d0aa64648 100644 --- a/AutomatedTesting/Levels/Base/Base.ly +++ b/AutomatedTesting/Levels/Base/Base.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4937547ca4c486ef59656314401933217e0e0401fec103e1fb91c25ec60a177 -size 2806 +oid sha256:a5f9e27e0f22c31ca61d866fb594c6fde5b8ceb891e17dda075fa1e0033ec2b9 +size 1666 diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 4f2e995ac4..ab80e7d23a 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -287,21 +287,22 @@ bool CCryDocManager::DoPromptFileName(QString& fileName, [[maybe_unused]] UINT n return false; } -CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAddToMRU) +CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions) { - assert(lpszFileName != nullptr); + assert(filename != nullptr); + const bool reopenIfSame = openSameLevelOptions == COpenSameLevelOptions::ReopenLevelIfSame; // find the highest confidence auto pos = m_templateList.begin(); CCrySingleDocTemplate::Confidence bestMatch = CCrySingleDocTemplate::noAttempt; CCrySingleDocTemplate* pBestTemplate = nullptr; CCryEditDoc* pOpenDocument = nullptr; - if (lpszFileName[0] == '\"') + if (filename[0] == '\"') { - ++lpszFileName; + ++filename; } - QString szPath = QString::fromUtf8(lpszFileName); + QString szPath = QString::fromUtf8(filename); if (szPath.endsWith('"')) { szPath.remove(szPath.length() - 1, 1); @@ -325,7 +326,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd } } - if (pOpenDocument != nullptr) + if (!reopenIfSame && pOpenDocument != nullptr) { return pOpenDocument; } @@ -336,7 +337,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd return nullptr; } - return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), bAddToMRU, false); + return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), addToMostRecentFileList, false); } ////////////////////////////////////////////////////////////////////////////// @@ -513,7 +514,7 @@ public: QString m_appRoot; QString m_logFile; QString m_pythonArgs; - QString m_pythontTestCase; + QString m_pythonTestCase; QString m_execFile; QString m_execLineCmd; @@ -562,7 +563,7 @@ public: const std::vector > stringOptions = { {{"logfile", "File name of the log file to write out to.", "logfile"}, m_logFile}, {{"runpythonargs", "Command-line argument string to pass to the python script if --runpython or --runpythontest was used.", "runpythonargs"}, m_pythonArgs}, - {{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythontTestCase}, + {{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythonTestCase}, {{"exec", "cfg file to run on startup, used for systems like automation", "exec"}, m_execFile}, {{"rhi", "Command-line argument to force which rhi to use", "dummyString"}, dummyString }, {{"rhi-device-validation", "Command-line argument to configure rhi validation", "dummyString"}, dummyString }, @@ -818,7 +819,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b return OpenDocumentFile(lpszPathName, true, bMakeVisible); } -CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, [[maybe_unused]] bool bMakeVisible) +CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, [[maybe_unused]] bool bMakeVisible) { CCryEditDoc* pCurDoc = GetIEditor()->GetDocument(); @@ -848,7 +849,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b { pCurDoc->OnOpenDocument(lpszPathName); pCurDoc->SetPathName(lpszPathName); - if (bAddToMRU) + if (addToMostRecentFileList) { CCryEditApp::instance()->AddToRecentFileList(lpszPathName); } @@ -1535,11 +1536,12 @@ void CCryEditApp::RunInitPythonScript(CEditCommandLineInfo& cmdInfo) { // Multiple testcases can be specified them with ';', these should match the files to run AZStd::vector testcaseList; + QByteArray pythonTestCase = cmdInfo.m_pythonTestCase.toUtf8(); testcaseList.resize(fileList.size()); { int i = 0; AzFramework::StringFunc::TokenizeVisitor( - fileStr.constData(), + pythonTestCase.constData(), [&i, &testcaseList](AZStd::string_view elem) { testcaseList[i++] = (elem); @@ -2630,7 +2632,7 @@ void CCryEditApp::OnShowHelpers() void CCryEditApp::OnEditLevelData() { auto dir = QFileInfo(GetIEditor()->GetDocument()->GetLevelPathName()).dir(); - CFileUtil::EditTextFile(dir.absoluteFilePath("LevelData.xml").toUtf8().data()); + CFileUtil::EditTextFile(dir.absoluteFilePath("leveldata.xml").toUtf8().data()); } ////////////////////////////////////////////////////////////////////////// @@ -3364,7 +3366,7 @@ void CCryEditApp::OnOpenSlice() } ////////////////////////////////////////////////////////////////////////// -CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName) +CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions) { if (m_openingLevel) { @@ -3404,9 +3406,9 @@ CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName) openDocTraceHandler.SetShowWindow(false); } - // in this case, we set bAddToMRU to always be true because adding files to the MRU list + // in this case, we set addToMostRecentFileList to always be true because adding files to the MRU list // automatically culls duplicate and normalizes paths anyway - m_pDocManager->OpenDocumentFile(lpszFileName, true); + m_pDocManager->OpenDocumentFile(filename, addToMostRecentFileList, openSameLevelOptions); if (openDocTraceHandler.HasAnyErrors()) { diff --git a/Code/Editor/CryEdit.h b/Code/Editor/CryEdit.h index 730af7c034..f68cfdc33d 100644 --- a/Code/Editor/CryEdit.h +++ b/Code/Editor/CryEdit.h @@ -85,6 +85,12 @@ public: using EditorIdleProcessingBus = AZ::EBus; +enum class COpenSameLevelOptions +{ + ReopenLevelIfSame, + NotReopenIfSame +}; + AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING class SANDBOX_API CCryEditApp @@ -174,7 +180,9 @@ public: virtual bool InitInstance(); virtual int ExitInstance(int exitCode = 0); virtual bool OnIdle(LONG lCount); - virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName); + virtual CCryEditDoc* OpenDocumentFile(const char* filename, + bool addToMostRecentFileList=true, + COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame); CCryDocManager* GetDocManager() { return m_pDocManager; } @@ -448,7 +456,7 @@ public: ~CCrySingleDocTemplate() {}; // avoid creating another CMainFrame // close other type docs before opening any things - virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, bool bMakeVisible); + virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, bool bMakeVisible); virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bMakeVisible = TRUE); virtual Confidence MatchDocType(const char* lpszPathName, CCryEditDoc*& rpDocMatch); @@ -468,7 +476,7 @@ public: virtual void OnFileNew(); virtual bool DoPromptFileName(QString& fileName, UINT nIDSTitle, DWORD lFlags, bool bOpenFileDialog, CDocTemplate* pTemplate); - virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName, bool bAddToMRU); + virtual CCryEditDoc* OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame); QVector m_templateList; }; diff --git a/Code/Editor/CryEditPy.cpp b/Code/Editor/CryEditPy.cpp index 1b38fe23b8..a25a497a4e 100644 --- a/Code/Editor/CryEditPy.cpp +++ b/Code/Editor/CryEditPy.cpp @@ -143,20 +143,11 @@ namespace return false; } } + const bool addToMostRecentFileList = false; + auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data(), + addToMostRecentFileList, COpenSameLevelOptions::ReopenLevelIfSame); - auto previousDocument = GetIEditor()->GetDocument(); - QString previousPathName = (previousDocument != nullptr) ? previousDocument->GetLevelPathName() : ""; - auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data()); - - // the underlying document pointer doesn't change, so we can't check that; use the path name's instead - - bool result = true; - if (newDocument == nullptr || newDocument->IsLevelLoadFailed() || (newDocument->GetLevelPathName() == previousPathName)) - { - result = false; - } - - return result; + return newDocument != nullptr && !newDocument->IsLevelLoadFailed(); } bool PyOpenLevelNoPrompt(const char* pLevelName) @@ -407,6 +398,11 @@ inline namespace Commands { return AZ::Debug::Trace::WaitForDebugger(timeoutSeconds); } + + AZStd::string PyGetFileAlias(AZStd::string alias) + { + return AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.c_str()); + } } namespace AzToolsFramework @@ -457,6 +453,8 @@ namespace AzToolsFramework addLegacyGeneral(behaviorContext->Method("attach_debugger", PyAttachDebugger, nullptr, "Prompts for attaching the debugger")); addLegacyGeneral(behaviorContext->Method("wait_for_debugger", PyWaitForDebugger, behaviorContext->MakeDefaultValues(-1.f), "Pauses this thread execution until the debugger has been attached")); + addLegacyGeneral(behaviorContext->Method("get_file_alias", PyGetFileAlias, nullptr, "Retrieves path for IO alias")); + // this will put these methods into the 'azlmbr.legacy.checkout_dialog' module auto addCheckoutDialog = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder) { diff --git a/Code/Editor/EditorPreferencesPageViewportCamera.cpp b/Code/Editor/EditorPreferencesPageViewportCamera.cpp index 16176bc241..2f6e6b1b9d 100644 --- a/Code/Editor/EditorPreferencesPageViewportCamera.cpp +++ b/Code/Editor/EditorPreferencesPageViewportCamera.cpp @@ -75,7 +75,10 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial ->Field("CaptureCursorLook", &CameraMovementSettings::m_captureCursorLook) ->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted) ->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX) - ->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY); + ->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY) + ->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX) + ->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY) + ->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ); serialize.Class() ->Version(2) @@ -154,7 +157,16 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial "Invert direction of pan in local Y axis") ->DataElement( AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor", - "Should the cursor be captured (hidden) while performing free look"); + "Should the cursor be captured (hidden) while performing free look") + ->DataElement( + AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position", + "Default X Camera Position when a level is opened") + ->DataElement( + AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position", + "Default Y Camera Position when a level is opened") + ->DataElement( + AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position", + "Default Z Camera Position when a level is opened"); editContext->Class("Camera Input Settings", "") ->DataElement( @@ -271,6 +283,12 @@ void CEditorPreferencesPage_ViewportCamera::OnApply() SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted); SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX); SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY); + SandboxEditor::SetDefaultCameraEditorPosition( + AZ::Vector3( + m_cameraMovementSettings.m_defaultCameraPositionX, + m_cameraMovementSettings.m_defaultCameraPositionY, + m_cameraMovementSettings.m_defaultCameraPositionZ + )); SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId); SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId); @@ -308,6 +326,11 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings() m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX(); m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY(); + AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition(); + m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX(); + m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY(); + m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ(); + m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName(); m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName(); m_cameraInputSettings.m_translateLeftChannelId = SandboxEditor::CameraTranslateLeftChannelId().GetName(); diff --git a/Code/Editor/EditorPreferencesPageViewportCamera.h b/Code/Editor/EditorPreferencesPageViewportCamera.h index fdc86b0f89..a2705bfd24 100644 --- a/Code/Editor/EditorPreferencesPageViewportCamera.h +++ b/Code/Editor/EditorPreferencesPageViewportCamera.h @@ -57,6 +57,9 @@ private: bool m_orbitYawRotationInverted; bool m_panInvertedX; bool m_panInvertedY; + float m_defaultCameraPositionX; + float m_defaultCameraPositionY; + float m_defaultCameraPositionZ; AZ::Crc32 RotateSmoothingVisibility() const { diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 2b54622d1c..8f2be1de6c 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -52,6 +52,9 @@ namespace SandboxEditor constexpr AZStd::string_view CameraOrbitDollyIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitDollyId"; constexpr AZStd::string_view CameraOrbitPanIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitPanId"; constexpr AZStd::string_view CameraFocusIdSetting = "/Amazon/Preferences/Editor/Camera/FocusId"; + constexpr AZStd::string_view CameraDefaultStartingPositionX = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/x"; + constexpr AZStd::string_view CameraDefaultStartingPositionY = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/y"; + constexpr AZStd::string_view CameraDefaultStartingPositionZ = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/z"; template void SetRegistry(const AZStd::string_view setting, T&& value) @@ -111,6 +114,21 @@ namespace SandboxEditor return AZStd::make_unique(); } + AZ::Vector3 DefaultEditorCameraPosition() + { + float xPosition = aznumeric_cast(GetRegistry(CameraDefaultStartingPositionX, 0.0)); + float yPosition = aznumeric_cast(GetRegistry(CameraDefaultStartingPositionY, -10.0)); + float zPosition = aznumeric_cast(GetRegistry(CameraDefaultStartingPositionZ, 4.0)); + return AZ::Vector3(xPosition, yPosition, zPosition); + } + + void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition) + { + SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX()); + SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY()); + SetRegistry(CameraDefaultStartingPositionZ, defaultCameraPosition.GetZ()); + } + AZ::u64 MaxItemsShownInAssetBrowserSearch() { return GetRegistry(AssetBrowserMaxItemsShownInSearchSetting, aznumeric_cast(50)); diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index c6f51cf461..9c3f0a46e5 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -12,6 +12,7 @@ #include #include +#include #include namespace SandboxEditor @@ -32,6 +33,9 @@ namespace SandboxEditor //! event will fire when a value in the settings registry (editorpreferences.setreg) is modified. SANDBOX_API AZStd::unique_ptr CreateEditorViewportSettingsCallbacks(); + SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition(); + SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition); + SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch(); SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown); diff --git a/Code/Editor/EditorViewportWidget.cpp b/Code/Editor/EditorViewportWidget.cpp index dad6734428..c7814cc842 100644 --- a/Code/Editor/EditorViewportWidget.cpp +++ b/Code/Editor/EditorViewportWidget.cpp @@ -112,7 +112,6 @@ void StartFixedCursorMode(QObject *viewport); #define RENDER_MESH_TEST_DISTANCE (0.2f) #define CURSOR_FONT_HEIGHT 8.0f - namespace AZ::ViewportHelpers { static const char TextCantCreateCameraNoLevel[] = "Cannot create camera when no level is loaded."; @@ -623,16 +622,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) PopDisableRendering(); { - AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero()); - AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); - float sx = terrainAabb.GetXExtent(); - float sy = terrainAabb.GetYExtent(); - Matrix34 viewTM; viewTM.SetIdentity(); - // Initial camera will be at middle of the map at the height of 2 - // meters above the terrain (default terrain height is 32) - viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f)); + + viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition())); SetViewTM(viewTM); UpdateScene(); @@ -647,16 +640,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) PopDisableRendering(); { - AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero()); - AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); - float sx = terrainAabb.GetXExtent(); - float sy = terrainAabb.GetYExtent(); - Matrix34 viewTM; viewTM.SetIdentity(); - // Initial camera will be at middle of the map at the height of 2 - // meters above the terrain (default terrain height is 32) - viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f)); + + viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition())); SetViewTM(viewTM); } break; @@ -1345,10 +1332,6 @@ void EditorViewportWidget::keyPressEvent(QKeyEvent* event) void EditorViewportWidget::SetViewTM(const Matrix34& tm) { - if (m_viewSourceType == ViewSourceType::None) - { - m_defaultViewTM = tm; - } SetViewTM(tm, false); } @@ -1445,6 +1428,10 @@ void EditorViewportWidget::SetViewTM(const Matrix34& camMatrix, bool bMoveOnly) "Please report this as a bug." ); } + else if (shouldUpdateObject == ShouldUpdateObject::No) + { + GetCurrentAtomView()->SetCameraTransform(LYTransformToAZMatrix3x4(camMatrix)); + } if (m_pressedKeyState == KeyPressedState::PressedThisFrame) { @@ -2028,6 +2015,9 @@ void EditorViewportWidget::SetDefaultCamera() m_viewSourceType = ViewSourceType::None; GetViewManager()->SetCameraObjectId(GUID_NULL); SetName(m_defaultViewName); + + // Set the default Editor Camera position. + m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition())); SetViewTM(m_defaultViewTM); // Synchronize the configured editor viewport FOV to the default camera @@ -2530,6 +2520,11 @@ bool EditorViewportSettings::StickySelectEnabled() const return SandboxEditor::StickySelectEnabled(); } +AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const +{ + return SandboxEditor::DefaultEditorCameraPosition(); +} + AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once); bool EditorViewportWidget::ShouldPreviewFullscreen() const @@ -2641,5 +2636,4 @@ void EditorViewportWidget::StopFullscreenPreview() // Show the main window MainWindow::instance()->show(); } - #include diff --git a/Code/Editor/EditorViewportWidget.h b/Code/Editor/EditorViewportWidget.h index 49930a2a13..68ea48c7f5 100644 --- a/Code/Editor/EditorViewportWidget.h +++ b/Code/Editor/EditorViewportWidget.h @@ -78,6 +78,7 @@ struct EditorViewportSettings : public AzToolsFramework::ViewportInteraction::Vi float ManipulatorLineBoundWidth() const override; float ManipulatorCircleBoundWidth() const override; bool StickySelectEnabled() const override; + AZ::Vector3 DefaultEditorCameraPosition() const override; }; // EditorViewportWidget window diff --git a/Code/Editor/GameExporter.cpp b/Code/Editor/GameExporter.cpp index 0324629877..ae1095b03a 100644 --- a/Code/Editor/GameExporter.cpp +++ b/Code/Editor/GameExporter.cpp @@ -31,11 +31,11 @@ #include ////////////////////////////////////////////////////////////////////////// -#define MUSIC_LEVEL_LIBRARY_FILE "Music.xml" -#define MATERIAL_LEVEL_LIBRARY_FILE "Materials.xml" -#define RESOURCE_LIST_FILE "ResourceList.txt" -#define USED_RESOURCE_LIST_FILE "UsedResourceList.txt" -#define SHADER_LIST_FILE "ShadersList.txt" +#define MUSIC_LEVEL_LIBRARY_FILE "music.xml" +#define MATERIAL_LEVEL_LIBRARY_FILE "materials.xml" +#define RESOURCE_LIST_FILE "resourcelist.txt" +#define USED_RESOURCE_LIST_FILE "usedresourcelist.txt" +#define SHADER_LIST_FILE "shaderslist.txt" #define GetAValue(rgb) ((BYTE)((rgb) >> 24)) @@ -185,9 +185,9 @@ bool CGameExporter::Export(unsigned int flags, [[maybe_unused]] EEndian eExportE ExportOcclusionMesh(sLevelPath.toUtf8().data()); //! Export Level data. - CLogFile::WriteLine("Exporting LevelData.xml"); + CLogFile::WriteLine("Exporting leveldata.xml"); ExportLevelData(sLevelPath); - CLogFile::WriteLine("Exporting LevelData.xml done."); + CLogFile::WriteLine("Exporting leveldata.xml done."); ExportLevelInfo(sLevelPath); @@ -266,26 +266,26 @@ void CGameExporter::ExportOcclusionMesh(const char* pszGamePath) void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/) { IEditor* pEditor = GetIEditor(); - pEditor->SetStatusText(QObject::tr("Exporting LevelData.xml...")); + pEditor->SetStatusText(QObject::tr("Exporting leveldata.xml...")); char versionString[256]; pEditor->GetFileVersion().ToString(versionString); - XmlNodeRef root = XmlHelpers::CreateXmlNode("LevelData"); + XmlNodeRef root = XmlHelpers::CreateXmlNode("leveldata"); root->setAttr("SandboxVersion", versionString); - XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("LevelDataAction"); + XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("leveldataaction"); rootAction->setAttr("SandboxVersion", versionString); ////////////////////////////////////////////////////////////////////////// // Save Level Data XML ////////////////////////////////////////////////////////////////////////// - QString levelDataFile = path + "LevelData.xml"; + QString levelDataFile = path + "leveldata.xml"; XmlString xmlData = root->getXML(); CCryMemFile file; file.Write(xmlData.c_str(), static_cast(xmlData.length())); m_levelPak.m_pakFile.UpdateFile(levelDataFile.toUtf8().data(), file); - QString levelDataActionFile = path + "LevelDataAction.xml"; + QString levelDataActionFile = path + "leveldataaction.xml"; XmlString xmlDataAction = rootAction->getXML(); CCryMemFile fileAction; fileAction.Write(xmlDataAction.c_str(), static_cast(xmlDataAction.length())); @@ -298,7 +298,7 @@ void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/ if (savedEntities) { QString entitiesFile; - entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "Mission0"); + entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "mission0"); m_levelPak.m_pakFile.UpdateFile(entitiesFile.toUtf8().data(), entitySaveBuffer.begin(), static_cast(entitySaveBuffer.size())); } } @@ -326,7 +326,7 @@ void CGameExporter::ExportLevelInfo(const QString& path) ////////////////////////////////////////////////////////////////////////// // Save LevelInfo file. ////////////////////////////////////////////////////////////////////////// - QString filename = path + "LevelInfo.xml"; + QString filename = path + "levelinfo.xml"; XmlString xmlData = root->getXML(); CCryMemFile file; diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index ea3653f663..275df11784 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -38,6 +38,8 @@ namespace UnitTest void BeginCursorCapture() override; void EndCursorCapture() override; bool IsMouseOver() const override; + void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override; + void ClearOverrideCursor() override; private: AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr; @@ -58,6 +60,17 @@ namespace UnitTest return true; } + void ViewportMouseCursorRequestImpl::SetOverrideCursor( + [[maybe_unused]] AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + // noop + } + + void ViewportMouseCursorRequestImpl::ClearOverrideCursor() + { + // noop + } + class ModularViewportCameraControllerFixture : public AllocatorsTestFixture { public: diff --git a/Code/Framework/AzCore/AzCore/Debug/Trace.cpp b/Code/Framework/AzCore/AzCore/Debug/Trace.cpp index cde8c36a4e..357149d096 100644 --- a/Code/Framework/AzCore/AzCore/Debug/Trace.cpp +++ b/Code/Framework/AzCore/AzCore/Debug/Trace.cpp @@ -577,7 +577,9 @@ namespace AZ } azstrcat(lines[i], AZ_ARRAY_SIZE(lines[i]), "\n"); - AZ_Printf(window, "%s", lines[i]); // feed back into the trace system so that listeners can get it. + // Use Output instead of AZ_Printf to be consistent with the exception output code and avoid + // this accidentally being suppressed as a normal message + Output(window, lines[i]); } } } diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFramework.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFramework.h index a911534dd9..5f0179d6ad 100644 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFramework.h +++ b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFramework.h @@ -46,6 +46,8 @@ namespace AzManipulatorTestFramework virtual void UpdateVisibility() = 0; //! Set if sticky select is enabled or not. virtual void SetStickySelect(bool enabled) = 0; + //! Get default Editor Camera Position. + virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0; }; //! This interface is used to simulate the manipulator manager while the manipulators are under test. diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h index cfecc0c91a..a8ba63500c 100644 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h +++ b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h @@ -36,6 +36,7 @@ namespace AzManipulatorTestFramework int GetViewportId() const override; void UpdateVisibility() override; void SetStickySelect(bool enabled) override; + AZ::Vector3 DefaultEditorCameraPosition() const override; // ViewportInteractionRequestBus overrides ... AzFramework::CameraState GetCameraState() override; diff --git a/Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp b/Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp index 509674e5c0..269baa703d 100644 --- a/Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp +++ b/Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp @@ -120,6 +120,11 @@ namespace AzManipulatorTestFramework m_stickySelect = enabled; } + AZ::Vector3 ViewportInteraction::DefaultEditorCameraPosition() const + { + return {}; + } + void ViewportInteraction::SetGridSize(float size) { m_gridSize = size; diff --git a/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.cpp b/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.cpp index 57b9d54559..d120e1fc51 100644 --- a/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.cpp @@ -65,7 +65,7 @@ namespace AzNetworking : m_delta(delta) , m_dataSerializer(m_delta.GetBufferPtr(), m_delta.GetBufferCapacity()) { - m_namePrefix.reserve(128); + ; } DeltaSerializerCreate::~DeltaSerializerCreate() @@ -73,7 +73,7 @@ namespace AzNetworking // Delete any left over records that might be hanging around for (auto iter : m_records) { - delete iter.second; + delete iter; } m_records.clear(); } @@ -160,28 +160,13 @@ namespace AzNetworking return SerializeHelper(buffer, bufferCapacity, isString, outSize, name); } - AZStd::string DeltaSerializerCreate::GetNextObjectName(const char* name) + bool DeltaSerializerCreate::BeginObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName) { - AZStd::string objectName = name; - objectName += "."; - objectName += AZStd::to_string(m_objectCounter); - ++m_objectCounter; - return objectName; - } - - bool DeltaSerializerCreate::BeginObject(const char* name, [[maybe_unused]] const char* typeName) - { - m_nameLengthStack.push_back(m_namePrefix.length()); - m_namePrefix += GetNextObjectName(name); - m_namePrefix += "."; return true; } bool DeltaSerializerCreate::EndObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName) { - const size_t prevLen = m_nameLengthStack.back(); - m_nameLengthStack.pop_back(); - m_namePrefix.resize(prevLen); return true; } @@ -205,25 +190,15 @@ namespace AzNetworking { typedef AbstractValue::ValueT ValueType; - const size_t prevLen = m_namePrefix.length(); - m_namePrefix += GetNextObjectName(name); - - const AZ::HashValue32 nameHash = AZ::TypeHash32(m_namePrefix.c_str()); - - m_namePrefix.resize(prevLen); - - AbstractValue::BaseValue*& baseValue = m_records[nameHash]; + AbstractValue::BaseValue* baseValue = m_records.size() > m_objectCounter ? m_records[m_objectCounter] : nullptr; + ++m_objectCounter; // If we are in the gather records phase, just save off the value records if (m_gatheringRecords) { - if (baseValue != nullptr) - { - AZ_Assert(false, "Duplicate name encountered in delta serializer. This will cause data to be serialized incorrectly."); - return false; - } - + AZ_Assert(baseValue == nullptr, "Expected to create a new record but found a pre-existing one at index %d", m_objectCounter - 1); baseValue = new ValueType(value); + m_records.push_back(baseValue); } else // If we are not gathering records, then we are comparing them { diff --git a/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.h b/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.h index 2b328931f5..47015ade7d 100644 --- a/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.h +++ b/Code/Framework/AzNetworking/AzNetworking/Serialization/DeltaSerializer.h @@ -90,8 +90,6 @@ namespace AzNetworking DeltaSerializerCreate(const DeltaSerializerCreate&) = delete; DeltaSerializerCreate& operator=(const DeltaSerializerCreate&) = delete; - AZStd::string GetNextObjectName(const char* name); - template bool SerializeHelper(T& value, uint32_t bufferCapacity, bool isString, uint32_t& outSize, const char* name); @@ -105,9 +103,7 @@ namespace AzNetworking bool m_gatheringRecords = false; uint32_t m_objectCounter = 0; - AZStd::string m_namePrefix; - AZStd::vector m_nameLengthStack; - AZStd::unordered_map m_records; + AZStd::vector m_records; NetworkInputSerializer m_dataSerializer; }; diff --git a/Code/Framework/AzNetworking/Tests/Serialization/DeltaSerializerTests.cpp b/Code/Framework/AzNetworking/Tests/Serialization/DeltaSerializerTests.cpp index ba59c91c21..32562d9940 100644 --- a/Code/Framework/AzNetworking/Tests/Serialization/DeltaSerializerTests.cpp +++ b/Code/Framework/AzNetworking/Tests/Serialization/DeltaSerializerTests.cpp @@ -11,4 +11,213 @@ namespace UnitTest { + struct DeltaDataElement + { + AzNetworking::PacketId m_packetId = AzNetworking::InvalidPacketId; + uint32_t m_id = 0; + AZ::TimeMs m_timeMs = AZ::TimeMs{ 0 }; + float m_blendFactor = 0.f; + AZStd::vector m_growVector, m_shrinkVector; + + bool Serialize(AzNetworking::ISerializer& serializer) + { + if (!serializer.Serialize(m_packetId, "PacketId") + || !serializer.Serialize(m_id, "Id") + || !serializer.Serialize(m_timeMs, "TimeMs") + || !serializer.Serialize(m_blendFactor, "BlendFactor") + || !serializer.Serialize(m_growVector, "GrowVector") + || !serializer.Serialize(m_shrinkVector, "ShrinkVector")) + { + return false; + } + + return true; + } + }; + + struct DeltaDataContainer + { + AZStd::string m_containerName; + AZStd::array m_container; + + // This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem + bool Serialize(AzNetworking::ISerializer& serializer) + { + // Always serialize the full first element + if(!m_container[0].Serialize(serializer)) + { + return false; + } + + for (uint32_t i = 1; i < m_container.size(); ++i) + { + if (serializer.GetSerializerMode() == AzNetworking::SerializerMode::WriteToObject) + { + AzNetworking::SerializerDelta deltaSerializer; + // Read out the delta + if (!deltaSerializer.Serialize(serializer)) + { + return false; + } + + // Start with previous value + m_container[i] = m_container[i - 1]; + // Then apply delta + AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer); + if (!applySerializer.ApplyDelta(m_container[i])) + { + return false; + } + } + else + { + AzNetworking::SerializerDelta deltaSerializer; + // Create the delta + AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer); + if (!createSerializer.CreateDelta(m_container[i - 1], m_container[i])) + { + return false; + } + + // Then write out the delta + if (!deltaSerializer.Serialize(serializer)) + { + return false; + } + } + } + + return true; + } + + // This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem + bool SerializeNoDelta(AzNetworking::ISerializer& serializer) + { + for (uint32_t i = 0; i < m_container.size(); ++i) + { + if(!m_container[i].Serialize(serializer)) + { + return false; + } + } + + return true; + } + }; + + class DeltaSerializerTests + : public UnitTest::AllocatorsTestFixture + { + public: + void SetUp() override + { + UnitTest::AllocatorsTestFixture::SetUp(); + } + + void TearDown() override + { + UnitTest::AllocatorsTestFixture::TearDown(); + } + }; + + static constexpr float BLEND_FACTOR_SCALE = 1.1f; + static constexpr uint32_t TIME_SCALE = 10; + + DeltaDataContainer TestDeltaContainer() + { + DeltaDataContainer testContainer; + AZStd::vector growVector, shrinkVector; + shrinkVector.resize(testContainer.m_container.array_size); + + testContainer.m_containerName = "TestContainer"; + for (int i = 0; i < testContainer.m_container.array_size; ++i) + { + testContainer.m_container[i].m_packetId = AzNetworking::PacketId(i); + testContainer.m_container[i].m_id = i; + testContainer.m_container[i].m_timeMs = AZ::TimeMs(i * TIME_SCALE); + testContainer.m_container[i].m_blendFactor = BLEND_FACTOR_SCALE * i; + growVector.push_back(i); + testContainer.m_container[i].m_growVector = growVector; + shrinkVector.resize(testContainer.m_container.array_size - i); + testContainer.m_container[i].m_shrinkVector = shrinkVector; + } + + return testContainer; + } + + TEST_F(DeltaSerializerTests, DeltaArray) + { + DeltaDataContainer inContainer = TestDeltaContainer(); + AZStd::array buffer; + AzNetworking::NetworkInputSerializer inSerializer(buffer.data(), static_cast(buffer.size())); + + // Always serialize the full first element + EXPECT_TRUE(inContainer.Serialize(inSerializer)); + + DeltaDataContainer outContainer; + AzNetworking::NetworkOutputSerializer outSerializer(buffer.data(), static_cast(buffer.size())); + + EXPECT_TRUE(outContainer.Serialize(outSerializer)); + + for (uint32_t i = 0; i > outContainer.m_container.size(); ++i) + { + EXPECT_EQ(inContainer.m_container[i].m_blendFactor, outContainer.m_container[i].m_blendFactor); + EXPECT_EQ(inContainer.m_container[i].m_id, outContainer.m_container[i].m_id); + EXPECT_EQ(inContainer.m_container[i].m_packetId, outContainer.m_container[i].m_packetId); + EXPECT_EQ(inContainer.m_container[i].m_timeMs, outContainer.m_container[i].m_timeMs); + EXPECT_EQ(inContainer.m_container[i].m_growVector[i], outContainer.m_container[i].m_growVector[i]); + EXPECT_EQ(inContainer.m_container[i].m_growVector.size(), outContainer.m_container[i].m_growVector.size()); + EXPECT_EQ(inContainer.m_container[i].m_shrinkVector.size(), outContainer.m_container[i].m_shrinkVector.size()); + } + } + + TEST_F(DeltaSerializerTests, DeltaSerializerCreateUnused) + { + // Every function here should return a constant value regardless of inputs + AzNetworking::SerializerDelta deltaSerializer; + AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer); + + EXPECT_EQ(createSerializer.GetCapacity(), 0); + EXPECT_EQ(createSerializer.GetSize(), 0); + EXPECT_EQ(createSerializer.GetBuffer(), nullptr); + EXPECT_EQ(createSerializer.GetSerializerMode(), AzNetworking::SerializerMode::ReadFromObject); + + createSerializer.ClearTrackedChangesFlag(); //NO-OP + EXPECT_FALSE(createSerializer.GetTrackedChangesFlag()); + EXPECT_TRUE(createSerializer.BeginObject("CreateSerializer", "Begin")); + EXPECT_TRUE(createSerializer.EndObject("CreateSerializer", "End")); + } + + TEST_F(DeltaSerializerTests, DeltaArraySize) + { + DeltaDataContainer deltaContainer = TestDeltaContainer(); + DeltaDataContainer noDeltaContainer = TestDeltaContainer(); + + AZStd::array deltaBuffer; + AzNetworking::NetworkInputSerializer deltaSerializer(deltaBuffer.data(), static_cast(deltaBuffer.size())); + AZStd::array noDeltaBuffer; + AzNetworking::NetworkInputSerializer noDeltaSerializer(noDeltaBuffer.data(), static_cast(noDeltaBuffer.size())); + + EXPECT_TRUE(deltaContainer.Serialize(deltaSerializer)); + EXPECT_FALSE(noDeltaContainer.SerializeNoDelta(noDeltaSerializer)); // Should run out of space + EXPECT_EQ(noDeltaSerializer.GetCapacity(), noDeltaSerializer.GetSize()); // Verify that the serializer filled up + EXPECT_FALSE(noDeltaSerializer.IsValid()); // and that it is no longer valid due to lack of space + } + + TEST_F(DeltaSerializerTests, DeltaSerializerApplyUnused) + { + // Every function here should return a constant value regardless of inputs + AzNetworking::SerializerDelta deltaSerializer; + AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer); + + EXPECT_EQ(applySerializer.GetCapacity(), 0); + EXPECT_EQ(applySerializer.GetSize(), 0); + EXPECT_EQ(applySerializer.GetBuffer(), nullptr); + EXPECT_EQ(applySerializer.GetSerializerMode(), AzNetworking::SerializerMode::WriteToObject); + + applySerializer.ClearTrackedChangesFlag(); //NO-OP + EXPECT_FALSE(applySerializer.GetTrackedChangesFlag()); + EXPECT_TRUE(applySerializer.BeginObject("CreateSerializer", "Begin")); + EXPECT_TRUE(applySerializer.EndObject("CreateSerializer", "End")); + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp index 18acccf049..574470ad6e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.cpp @@ -244,6 +244,17 @@ namespace AzToolsFramework const auto eventType = event->type(); + if (eventType == QEvent::Type::MouseMove) + { + // clear override cursor when moving outside of the viewport + const auto* mouseEvent = static_cast(event); + if (m_overrideCursor && !m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(mouseEvent->globalPos()))) + { + qApp->restoreOverrideCursor(); + m_overrideCursor = false; + } + } + // Only accept mouse & key release events that originate from an object that is not our target widget, // as we don't want to erroneously intercept user input meant for another component. if (object != m_sourceWidget && eventType != QEvent::Type::KeyRelease && eventType != QEvent::Type::MouseButtonRelease) @@ -262,7 +273,7 @@ namespace AzToolsFramework if (eventType == QEvent::FocusIn) { const auto globalCursorPosition = QCursor::pos(); - if (m_sourceWidget->geometry().contains(globalCursorPosition)) + if (m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(globalCursorPosition))) { HandleMouseMoveEvent(globalCursorPosition); } @@ -452,4 +463,32 @@ namespace AzToolsFramework } } } + + static Qt::CursorShape QtCursorFromAzCursor(const ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + switch (cursorStyleOverride) + { + case ViewportInteraction::CursorStyleOverride::Forbidden: + return Qt::ForbiddenCursor; + default: + return Qt::ArrowCursor; + } + } + + void QtEventToAzInputMapper::SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + ClearOverrideCursor(); + + qApp->setOverrideCursor(QtCursorFromAzCursor(cursorStyleOverride)); + m_overrideCursor = true; + } + + void QtEventToAzInputMapper::ClearOverrideCursor() + { + if (m_overrideCursor) + { + qApp->restoreOverrideCursor(); + m_overrideCursor = false; + } + } } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h index 5add24ad84..4cb391e63b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputManager.h @@ -15,10 +15,11 @@ #include #include #include - #include #include +#include + #include #include #include @@ -55,6 +56,9 @@ namespace AzToolsFramework //! like a dolly or rotation, where mouse movement is important but cursor location is not. void SetCursorCaptureEnabled(bool enabled); + void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride); + void ClearOverrideCursor(); + // QObject overrides... bool eventFilter(QObject* object, QEvent* event) override; @@ -164,6 +168,8 @@ namespace AzToolsFramework bool m_enabled = true; // Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement). bool m_capturingCursor = false; + // Flags whether the cursor has been overridden. + bool m_overrideCursor = false; // Our viewport-specific AZ devices. We control their internal input channel states. AZStd::unique_ptr m_mouseDevice; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h index 9d2f7e9b90..8ec772f0da 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h @@ -198,6 +198,8 @@ namespace AzToolsFramework virtual float ManipulatorCircleBoundWidth() const = 0; //! Returns if sticky select is enabled or not. virtual bool StickySelectEnabled() const = 0; + //! Returns the default viewport camera position. + virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0; protected: ~ViewportSettingsRequests() = default; @@ -300,6 +302,12 @@ namespace AzToolsFramework using EditorViewportInputTimeNowRequestBus = AZ::EBus; + //! The style of cursor override. + enum class CursorStyleOverride + { + Forbidden + }; + //! Viewport requests for managing the viewport cursor state. class ViewportMouseCursorRequests { @@ -310,6 +318,10 @@ namespace AzToolsFramework virtual void EndCursorCapture() = 0; //! Is the mouse over the viewport. virtual bool IsMouseOver() const = 0; + //! Set the cursor style override. + virtual void SetOverrideCursor(CursorStyleOverride cursorStyleOverride) = 0; + //! Clear the cursor style override. + virtual void ClearOverrideCursor() = 0; protected: ~ViewportMouseCursorRequests() = default; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp index 94795c44a9..0f94b95e5f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp @@ -44,6 +44,13 @@ AZ_CVAR( nullptr, AZ::ConsoleFunctorFlags::Null, "Display the aggregate world bounds for a given entity (the union of all world component Aabbs)"); +AZ_CVAR( + bool, + ed_useCursorLockIconInFocusMode, + false, + nullptr, + AZ::ConsoleFunctorFlags::Null, + "Use a lock icon when the cursor is over entities that cannot be interacted with"); namespace AzToolsFramework { @@ -222,6 +229,13 @@ namespace AzToolsFramework // verify if the entity Id corresponds to an entity that is focused; if not, halt selection. if (entityIdUnderCursor.IsValid() && !IsSelectableAccordingToFocusMode(entityIdUnderCursor)) { + if (ed_useCursorLockIconInFocusMode) + { + ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor, + ViewportInteraction::CursorStyleOverride::Forbidden); + } + if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() && mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down || mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick) @@ -232,6 +246,9 @@ namespace AzToolsFramework return CursorEntityIdQuery(AZ::EntityId(), AZ::EntityId()); } + ViewportInteraction::ViewportMouseCursorRequestBus::Event( + viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::ClearOverrideCursor); + // container entity support - if the entity that is being selected is part of a closed container, // change the selection to the container instead. if (ContainerEntityInterface* containerEntityInterface = AZ::Interface::Get()) diff --git a/Code/Framework/AzToolsFramework/Tests/UI/EntityOutlinerTests.cpp b/Code/Framework/AzToolsFramework/Tests/UI/EntityOutlinerTests.cpp index 614959d7e0..fa8de64c62 100644 --- a/Code/Framework/AzToolsFramework/Tests/UI/EntityOutlinerTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/UI/EntityOutlinerTests.cpp @@ -90,8 +90,7 @@ namespace UnitTest // Update our undo cache entry to include the rename / reparent as one atomic operation. m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop()); - // Force a prefab propagation as updates are deferred to the next tick. - m_prefabSystemComponent->OnSystemTick(); + ProcessDeferredUpdates(); return entityId; } @@ -125,6 +124,30 @@ namespace UnitTest return m_model->index(0, 0); } + // Kicks off any updates scheduled for the next tick + void ProcessDeferredUpdates() + { + // Force a prefab propagation for updates that are deferred to the next tick. + m_prefabSystemComponent->OnSystemTick(); + + // Ensure the model process its entity update queue + m_model->ProcessEntityUpdates(); + } + + // Performs an undo operation and ensures the tick-scheduled updates happen + void Undo() + { + m_undoStack->Undo(); + ProcessDeferredUpdates(); + } + + // Performs a redo operation and ensures the tick-scheduled updates happen + void Redo() + { + m_undoStack->Redo(); + ProcessDeferredUpdates(); + } + AZStd::unique_ptr m_model; AZStd::unique_ptr m_modelTester; AzToolsFramework::UndoSystem::UndoStack* m_undoStack = nullptr; @@ -139,21 +162,18 @@ namespace UnitTest CreateNamedEntity(AZStd::string::format("Entity%zu", i)); EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1); } - m_model->ProcessEntityUpdates(); for (int i = entityCount; i > 0; --i) { - m_undoStack->Undo(); + Undo(); EXPECT_EQ(m_model->rowCount(GetRootIndex()), i - 1); } - m_model->ProcessEntityUpdates(); for (size_t i = 0; i < entityCount; ++i) { - m_undoStack->Redo(); + Redo(); EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1); } - m_model->ProcessEntityUpdates(); } TEST_F(EntityOutlinerTest, TestCreateNestedHierarchyUndoAndRedoWorks) @@ -177,21 +197,18 @@ namespace UnitTest { parentId = CreateNamedEntity(AZStd::string::format("EntityDepth%i", i), parentId); EXPECT_EQ(modelDepth(), i + 1); - m_model->ProcessEntityUpdates(); } for (int i = depth - 1; i >= 0; --i) { - m_undoStack->Undo(); + Undo(); EXPECT_EQ(modelDepth(), i); - m_model->ProcessEntityUpdates(); } for (int i = 0; i < depth; ++i) { - m_undoStack->Redo(); + Redo(); EXPECT_EQ(modelDepth(), i + 1); - m_model->ProcessEntityUpdates(); } } } // namespace UnitTest diff --git a/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp b/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp index 49af5080ec..3a2bba64d3 100644 --- a/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp +++ b/Code/Legacy/CrySystem/LevelSystem/LevelSystem.cpp @@ -86,7 +86,7 @@ bool CLevelInfo::ReadInfo() usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled); // Set up a default game type for legacy code. - m_defaultGameTypeName = "Mission0"; + m_defaultGameTypeName = "mission0"; if (usePrefabSystemForLevels) { @@ -96,17 +96,17 @@ bool CLevelInfo::ReadInfo() AZStd::string levelPath(m_levelPath); AZStd::string xmlFile(levelPath); - xmlFile += "/LevelInfo.xml"; + xmlFile += "/levelinfo.xml"; XmlNodeRef rootNode = GetISystem()->LoadXmlFromFile(xmlFile.c_str()); if (rootNode) { AZStd::string dataFile(levelPath); - dataFile += "/LevelDataAction.xml"; + dataFile += "/leveldataaction.xml"; XmlNodeRef dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str()); if (!dataNode) { - dataFile = levelPath + "/LevelData.xml"; + dataFile = levelPath + "/leveldata.xml"; dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str()); } @@ -614,7 +614,7 @@ ILevel* CLevelSystem::LoadLevelInternal(const char* _levelName) } { - AZStd::string missionXml("Mission_"); + AZStd::string missionXml("mission_"); missionXml += pLevelInfo->m_defaultGameTypeName; missionXml += ".xml"; AZStd::string xmlFile(pLevelInfo->GetPath()); diff --git a/Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp b/Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp index 4cd4489a4c..fc806f6c9e 100644 --- a/Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp +++ b/Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp @@ -3573,19 +3573,61 @@ namespace AssetProcessor QString knownPathBeforeWildcard = encodedFileData.left(slashBeforeWildcardIndex + 1); // include the slash QString relativeSearch = encodedFileData.mid(slashBeforeWildcardIndex + 1); // skip the slash - for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i) + // Absolute path, just check the 1 scan folder + if (AZ::IO::PathView(encodedFileData.toUtf8().constData()).IsAbsolute()) { - const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i); - - if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/")) + QString scanFolderName; + if (!m_platformConfig->ConvertToRelativePath(encodedFileData, resultDatabaseSourceName, scanFolderName)) { - continue; + AZ_Warning( + AssetProcessor::ConsoleChannel, false, + "'%s' does not appear to be in any input folder. Use relative paths instead.", + sourceDependency.m_sourceFileDependencyPath.c_str()); } - QDir rooted(scanFolderInfo->ScanPath()); - QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard); + auto scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolderName); + + // Make an absolute path that is ScanFolderPath + Part of search path before the wildcard + QDir rooted(scanFolderName); + QString scanFolderAndKnownSubPath = rooted.absoluteFilePath(knownPathBeforeWildcard); + + resolvedDependencyList.append(m_platformConfig->FindWildcardMatches( + scanFolderAndKnownSubPath, relativeSearch, false, scanFolderInfo->RecurseSubFolders())); + } + else // Relative path, check every scan folder + { + for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i) + { + const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i); + + if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/")) + { + continue; + } - resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders())); + QDir rooted(scanFolderInfo->ScanPath()); + QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard); + + resolvedDependencyList.append(m_platformConfig->FindWildcardMatches( + absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders())); + } + } + + // Convert to relative paths + for (auto dependencyItr = resolvedDependencyList.begin(); dependencyItr != resolvedDependencyList.end();) + { + QString relativePath, scanFolder; + if (m_platformConfig->ConvertToRelativePath(*dependencyItr, relativePath, scanFolder)) + { + *dependencyItr = relativePath; + ++dependencyItr; + } + else + { + AZ_Warning("AssetProcessor", false, "Failed to get relative path for wildcard dependency file %s. Is the file within a scan folder?", + dependencyItr->toUtf8().constData()); + dependencyItr = resolvedDependencyList.erase(dependencyItr); + } } resultDatabaseSourceName = encodedFileData.replace('\\', '/'); diff --git a/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp b/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp index 4a033cf6ca..9a60bf3110 100644 --- a/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp +++ b/Code/Tools/AssetProcessor/native/utilities/PlatformConfiguration.cpp @@ -1435,32 +1435,35 @@ namespace AssetProcessor return QString(); } - QStringList PlatformConfiguration::FindWildcardMatches(const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const + QStringList PlatformConfiguration::FindWildcardMatches( + const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const { if (relativeName.isEmpty()) { return QStringList(); } - const int pathLen = sourceFolder.length() + 1; + QDir sourceFolderDir(sourceFolder); - relativeName.replace('\\', '/'); + QString posixRelativeName = QDir::fromNativeSeparators(relativeName); QStringList returnList; - QRegExp nameMatch{ relativeName, Qt::CaseInsensitive, QRegExp::Wildcard }; - QDirIterator diretoryIterator(sourceFolder, QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags); + QRegExp nameMatch{ posixRelativeName, Qt::CaseInsensitive, QRegExp::Wildcard }; + QDirIterator dirIterator( + sourceFolderDir.path(), QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, + recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags); QStringList files; - while (diretoryIterator.hasNext()) + while (dirIterator.hasNext()) { - diretoryIterator.next(); - if (!includeFolders && !diretoryIterator.fileInfo().isFile()) + dirIterator.next(); + if (!includeFolders && !dirIterator.fileInfo().isFile()) { continue; } - QString pathMatch{ diretoryIterator.filePath().mid(pathLen) }; + QString pathMatch{ sourceFolderDir.relativeFilePath(dirIterator.filePath()) }; if (nameMatch.exactMatch(pathMatch)) { - returnList.append(AssetUtilities::NormalizeFilePath(diretoryIterator.filePath())); + returnList.append(QDir::fromNativeSeparators(dirIterator.filePath())); } } return returnList; diff --git a/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuLinks.h b/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuLinks.h index c52377c50a..cd56978d16 100644 --- a/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuLinks.h +++ b/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuLinks.h @@ -51,4 +51,21 @@ namespace AWSCore "https://o3de.org/docs/user-guide/gems/reference/aws/aws-metrics/advanced-topics/"; static constexpr const char AWSMetricsSettingsUrl[] = "https://o3de.org/docs/user-guide/gems/reference/aws/aws-metrics/"; + + static constexpr const char AWSGameLiftGemOverviewUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/"; + static constexpr const char AWSGameLiftGemSetupUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/gem-setup/"; + static constexpr const char AWSGameLiftScriptingUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/scripting/"; + static constexpr const char AWSGameLiftAPIReferenceUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/cpp-api/"; + static constexpr const char AWSGameLiftAdvancedTopicsUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/advanced-topics/"; + static constexpr const char AWSGameLiftLocalTestingUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/local-testing/"; + static constexpr const char AWSGameLiftBuildPackagingUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/build-packaging-for-windows/"; + static constexpr const char AWSGameLiftResourceManagementUrl[] = + "https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/resource-management/"; } // namespace AWSCore diff --git a/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuNames.h b/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuNames.h index ce125ecbe6..10a61f91fd 100644 --- a/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuNames.h +++ b/Gems/AWSCore/Code/Include/Private/Editor/Constants/AWSCoreEditorMenuNames.h @@ -38,4 +38,14 @@ namespace AWSCore static constexpr const char AWSMetricsAPIReferenceActionText[] = "API reference"; static constexpr const char AWSMetricsAdvancedTopicsActionText[] = "Advanced topics"; static constexpr const char AWSMetricsSettingsActionText[] = "Metrics settings"; + + static constexpr const char AWSGameLiftActionText[] = "GameLift Gem"; + static constexpr const char AWSGameLiftGemOverviewActionText[] = "Gem overview"; + static constexpr const char AWSGameLiftGemSetupActionText[] = "Setup"; + static constexpr const char AWSMGameLiftScriptingActionText[] = "Scripting reference"; + static constexpr const char AWSGameLiftAPIReferenceActionText[] = "API reference"; + static constexpr const char AWSGameLiftAdvancedTopicsActionText[] = "Advanced topics"; + static constexpr const char AWSGameLiftLocalTestingActionText[] = "Local testing"; + static constexpr const char AWSGameLiftBuildPackagingActionText[] = "Build packaging (Windows)"; + static constexpr const char AWSGameLiftResourceManagementActionText[] = "Resource Management"; } // namespace AWSCore diff --git a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h index 62f91d36ef..33ec0c6d27 100644 --- a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h +++ b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h @@ -48,6 +48,7 @@ namespace AWSCore // AWSCoreEditorRequestBus interface implementation void SetAWSClientAuthEnabled() override; void SetAWSMetricsEnabled() override; + void SetAWSGameLiftEnabled() override; QMenu* SetAWSFeatureSubMenu(const AZStd::string& menuText); diff --git a/Gems/AWSCore/Code/Include/Public/AWSCoreBus.h b/Gems/AWSCore/Code/Include/Public/AWSCoreBus.h index 824aa2aebb..e46e90e0a6 100644 --- a/Gems/AWSCore/Code/Include/Public/AWSCoreBus.h +++ b/Gems/AWSCore/Code/Include/Public/AWSCoreBus.h @@ -53,6 +53,7 @@ namespace AWSCore public: virtual void SetAWSClientAuthEnabled() = 0; virtual void SetAWSMetricsEnabled() = 0; + virtual void SetAWSGameLiftEnabled() = 0; ////////////////////////////////////////////////////////////////////////// // EBusTraits overrides diff --git a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp index 6391dd94ee..e397b8642c 100644 --- a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp +++ b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp @@ -156,6 +156,11 @@ namespace AWSCore metrics->setIcon(QIcon(QString(":/Notifications/download.svg"))); metrics->setDisabled(true); this->addAction(metrics); + + QAction* gamelift = new QAction(QObject::tr(AWSGameLiftActionText)); + gamelift->setIcon(QIcon(QString(":/Notifications/download.svg"))); + gamelift->setDisabled(true); + this->addAction(gamelift); } void AWSCoreEditorMenu::SetAWSClientAuthEnabled() @@ -181,6 +186,23 @@ namespace AWSCore AddSpaceForIcon(subMenu); } + void AWSCoreEditorMenu::SetAWSGameLiftEnabled() + { + // TODO: instead of creating submenu in core editor, aws feature gem should return submenu component directly + QMenu* subMenu = SetAWSFeatureSubMenu(AWSGameLiftActionText); + + subMenu->addAction(AddExternalLinkAction(AWSGameLiftGemOverviewActionText, AWSGameLiftGemOverviewUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftGemSetupActionText, AWSGameLiftGemSetupUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSMGameLiftScriptingActionText, AWSGameLiftScriptingUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftAPIReferenceActionText, AWSGameLiftAPIReferenceUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftAdvancedTopicsActionText, AWSGameLiftAdvancedTopicsUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftLocalTestingActionText, AWSGameLiftLocalTestingUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftBuildPackagingActionText, AWSGameLiftBuildPackagingUrl, ":/Notifications/link.svg")); + subMenu->addAction(AddExternalLinkAction(AWSGameLiftResourceManagementActionText, AWSGameLiftResourceManagementUrl, ":/Notifications/link.svg")); + + AddSpaceForIcon(subMenu); + } + void AWSCoreEditorMenu::SetAWSMetricsEnabled() { // TODO: instead of creating submenu in core editor, aws feature gem should return submenu component directly diff --git a/Gems/AWSCore/Code/Tests/Editor/UI/AWSCoreEditorMenuTest.cpp b/Gems/AWSCore/Code/Tests/Editor/UI/AWSCoreEditorMenuTest.cpp index e9453a4a0a..0f643f3cc8 100644 --- a/Gems/AWSCore/Code/Tests/Editor/UI/AWSCoreEditorMenuTest.cpp +++ b/Gems/AWSCore/Code/Tests/Editor/UI/AWSCoreEditorMenuTest.cpp @@ -21,8 +21,8 @@ using namespace AWSCore; -static constexpr const int ExpectedActionNumOnWindowsPlatform = 8; -static constexpr const int ExpectedActionNumOnOtherPlatform = 6; +static constexpr const int ExpectedActionNumOnWindowsPlatform = 9; +static constexpr const int ExpectedActionNumOnOtherPlatform = 7; class AWSCoreEditorMenuTest : public AWSCoreFixture @@ -60,6 +60,7 @@ TEST_F(AWSCoreEditorMenuTest, AWSCoreEditorMenu_BroadcastFeatureGemsAreEnabled_C AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSClientAuthEnabled); AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSMetricsEnabled); + AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSGameLiftEnabled); QList actualActions = testMenu.actions(); for (QList::iterator itr = actualActions.begin(); itr != actualActions.end(); itr++) @@ -73,5 +74,10 @@ TEST_F(AWSCoreEditorMenuTest, AWSCoreEditorMenu_BroadcastFeatureGemsAreEnabled_C { EXPECT_TRUE((*itr)->isEnabled()); } + + if (QString::compare((*itr)->text(), AWSGameLiftActionText) == 0) + { + EXPECT_TRUE((*itr)->isEnabled()); + } } } diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp index 6ab998b696..981c1599c9 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Source/AWSGameLiftClientSystemComponent.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -105,6 +106,8 @@ namespace AWSGameLift m_gameliftClient.reset(); m_gameliftManager->ActivateManager(); m_gameliftTicketTracker->ActivateTracker(); + + AWSCore::AWSCoreEditorRequestBus::Broadcast(&AWSCore::AWSCoreEditorRequests::SetAWSGameLiftEnabled); } void AWSGameLiftClientSystemComponent::Deactivate() diff --git a/Gems/Atom/Component/DebugCamera/Code/Source/CameraComponent.cpp b/Gems/Atom/Component/DebugCamera/Code/Source/CameraComponent.cpp index ca62918aa3..f3123e2b38 100644 --- a/Gems/Atom/Component/DebugCamera/Code/Source/CameraComponent.cpp +++ b/Gems/Atom/Component/DebugCamera/Code/Source/CameraComponent.cpp @@ -272,7 +272,10 @@ namespace AZ else if (m_componentConfig.m_target) { const auto& viewport = m_componentConfig.m_target->GetViewport(); - m_aspectRatio = viewport.m_maxX / viewport.m_maxY; + if (viewport.m_maxX > 0.0f && viewport.m_maxY > 0.0f) + { + m_aspectRatio = viewport.m_maxX / viewport.m_maxY; + } } } diff --git a/Gems/Atom/Feature/Common/Assets/Passes/HDRColorGrading.pass b/Gems/Atom/Feature/Common/Assets/Passes/HDRColorGrading.pass index adcd43ce83..c043d1e72b 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/HDRColorGrading.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/HDRColorGrading.pass @@ -73,7 +73,7 @@ }, { "Name": "m_colorGradingPreSaturation", - "Value": 1.0 // -100 ... 100 + "Value": 1.0 // 0 ... 2 }, { "Name": "m_colorFilterIntensity", @@ -101,7 +101,7 @@ }, { "Name": "m_colorGradingPostSaturation", - "Value": 1.0 // -100 ... 100 + "Value": 1.0 // 0 ... 2 }, { "Name": "m_smhShadowsStart", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass index eec3634045..2347aeb811 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass @@ -105,6 +105,11 @@ } ] }, + { + "Name": "LutGenerationPass", + "TemplateName": "LutGenerationTemplate", + "Enabled": true + }, { "Name": "LookModificationTransformPass", "TemplateName": "LookModificationTransformTemplate", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LutGeneration.pass b/Gems/Atom/Feature/Common/Assets/Passes/LutGeneration.pass new file mode 100644 index 0000000000..4a8735cf7c --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/LutGeneration.pass @@ -0,0 +1,47 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "LutGenerationTemplate", + "PassClass": "LutGenerationPass", + "Slots": [ + { + "Name": "LutOutput", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "DontCare" + } + } + ], + "ImageAttachments": [ + { + "Name": "ColorGradingLut", + "ImageDescriptor": { + "Format": "R32G32B32A32_FLOAT", + "BindFlags": [ + "ShaderWrite" + ] + } + } + ], + "Connections": [ + { + "LocalSlot": "LutOutput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "ColorGradingLut" + } + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "Shaders/ColorGrading/LutGeneration.shader" + } + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset index 76285b00c7..7770b326a6 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset +++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset @@ -511,7 +511,11 @@ { "Name": "HDRColorGradingTemplate", "Path": "Passes/HDRColorGrading.pass" - } + }, + { + "Name": "LutGenerationTemplate", + "Path": "Passes/LutGeneration.pass" + } ] } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli index e84b18c550..1528bdc63e 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ColorManagement/GeneratedTransforms/AcesCcToAcesCg.azsli @@ -59,7 +59,7 @@ UNDISCLOSED. #pragma once -static const float HALF_MAX = 65504.0f; +#include float AcesCcToLinear(float value) { 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 new file mode 100644 index 0000000000..099a6394d4 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl @@ -0,0 +1,154 @@ +/* + * 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 <3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli> +#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli> +#include <3rdParty/Features/PostProcessing/KelvinToRgb.azsli> + +static const float FloatEpsilon = 1.192092896e-07; // 1.0 + FloatEpsilon != 1.0, smallest positive float +static const float FloatMin = FLOAT_32_MIN; // Min float number that is positive +static const float FloatMax = FLOAT_32_MAX; // Max float number representable + +static const float AcesCcMidGrey = 0.4135884; + +float SaturateWithEpsilon(float value) +{ + return clamp(value, FloatEpsilon, 1.0f); +} + +// Below are the color grading functions. These expect the frame color to be in ACEScg space. +// Note that some functions may have some quirks in their implementation and is subject to change. +float3 ColorGradePostExposure (float3 frameColor, float exposure) +{ + frameColor *= pow(2.0f, exposure); + return frameColor; +} + +// The contrast equation is performed in ACEScc (logarithmic) color space. +float3 ColorGradingContrast (float3 frameColor, float midgrey, float amount) +{ + const float contrastAdjustment = amount * 0.01f + 1.0f; + frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScg, ColorSpaceId::ACEScc); + frameColor = (frameColor - midgrey) * contrastAdjustment + midgrey; + return frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScc, ColorSpaceId::ACEScg); +} + +// The swatchColor param expects a linear RGB value. +float3 ColorGradeColorFilter (float3 frameColor, float3 swatchColor, float alpha, float colorFilterIntensity) +{ + swatchColor = TransformColor(swatchColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + swatchColor *= pow(2.0f, colorFilterIntensity); + const float3 frameAdjust = frameColor * swatchColor; + return frameColor = lerp(frameColor, frameAdjust, alpha); +} + +float3 ColorGradeHueShift (float3 frameColor, float amount) +{ + float3 frameHsv = RgbToHsv(frameColor); + const float hue = frameHsv.x + amount; + frameHsv.x = RotateHue(hue, 0.0, 1.0); + return HsvToRgb(frameHsv); +} + +float3 ColorGradeSaturation (float3 frameColor, float control) +{ + const float vLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); + return (frameColor - vLuminance) * control + vLuminance; +} + +float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin) +{ + const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); + const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL + return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance +} + +// pow(f, e) won't work if f is negative, or may cause inf/NAN. +float3 NoNanPow(float3 base, float3 power) +{ + return pow(max(abs(base), float3(FloatEpsilon, FloatEpsilon, FloatEpsilon)), power); +} + +float3 ColorGradeSplitTone ( + float3 frameColor, + float balance, + float weight, + float3 splitToneShadowsColor, + float3 splitToneHighlightsColor) +{ + float3 frameSplitTone = NoNanPow(frameColor, 1.0 / 2.2); + const float t = SaturateWithEpsilon(CalculateLuminance(SaturateWithEpsilon(frameSplitTone), ColorSpaceId::ACEScg) + balance); + const float3 shadows = lerp(0.5, splitToneShadowsColor, 1.0 - t); + const float3 highlights = lerp(0.5, splitToneHighlightsColor, t); + frameSplitTone = BlendMode_SoftLight(frameSplitTone, shadows); + frameSplitTone = BlendMode_SoftLight(frameSplitTone, highlights); + frameSplitTone = NoNanPow(frameSplitTone, 2.2); + return lerp(frameColor.rgb, frameSplitTone.rgb, weight); +} + +float3 ColorGradeChannelMixer ( + float3 frameColor, + float3 channelMixingRed, + float3 channelMixingGreen, + float3 channelMixingBlue) +{ + return mul(float3x3(channelMixingRed, + channelMixingGreen, + channelMixingBlue), + frameColor); +} + +float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStart, float shadowsEnd, + float highlightsStart, float highlightsEnd, float weight, + float4 shadowsColor, float4 midtonesColor, float4 highlightsColor) +{ + const float3 shadowsColorACEScg = TransformColor(shadowsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + const float3 midtonesColorACEScg = TransformColor(midtonesColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + const float3 highlightsColorACEScg = TransformColor(highlightsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + + const float cLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); + const float shadowsWeight = 1.0 - smoothstep(shadowsStart, shadowsEnd, cLuminance); + const float highlightsWeight = smoothstep(highlightsStart, highlightsEnd, cLuminance); + const float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight; + + const float3 frameSmh = frameColor * shadowsColorACEScg * shadowsWeight + + frameColor * midtonesColorACEScg * midtonesWeight + + frameColor * highlightsColorACEScg * highlightsWeight; + return lerp(frameColor.rgb, frameSmh.rgb, weight); +} + +// perform color grading in ACEScg space +float3 ColorGrade(float3 frameColor) +{ + frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin), PassSrg::m_whiteBalanceWeight); + frameColor = lerp(frameColor, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb, + 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 = 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); + frameColor = max(frameColor, 0.0); + frameColor = ColorGradeShadowsMidtonesHighlights(frameColor, PassSrg::m_smhShadowsStart, PassSrg::m_smhShadowsEnd, + 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); + return max(frameColor.rgb, 0.0); +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl new file mode 100644 index 0000000000..6a76f58519 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.azsl @@ -0,0 +1,113 @@ +/* + * 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 + +float3 convert2Dto3DLutCoords(float2 uv, float width, float height) +{ + // convert from center pixel uvs to [0,1] + float offset = 1.0/height/2.0; + float scale = 1.0 - offset*2.0; + + float2 adjustedUv = float2(uv.x * width, uv.y * height); + float3 coords = float3(adjustedUv.x%height, 0.5 + int(adjustedUv.x/height), adjustedUv.y)/height; + return (coords - offset)/scale; +} + +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // framebuffer sampler + Sampler PointSampler + { + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + int m_lutResolution; + int m_shaperType; + float m_shaperBias; + float m_shaperScale; + + float m_colorAdjustmentWeight; + float m_colorGradingExposure; + float m_colorGradingContrast; + float m_colorGradingPreSaturation; + float m_colorFilterIntensity; + float m_colorFilterMultiply; + float4 m_colorFilterSwatch; + + float m_whiteBalanceWeight; + float m_whiteBalanceKelvin; + float m_whiteBalanceTint; + + float m_splitToneBalance; + float m_splitToneWeight; + float4 m_splitToneShadowsColor; + float4 m_splitToneHighlightsColor; + + float m_smhShadowsStart; + float m_smhShadowsEnd; + float m_smhHighlightsStart; + float m_smhHighlightsEnd; + float m_smhWeight; + float4 m_smhShadowsColor; + float4 m_smhMidtonesColor; + float4 m_smhHighlightsColor; + + float3 m_channelMixingRed; + float3 m_channelMixingGreen; + float3 m_channelMixingBlue; + + float m_finalAdjustmentWeight; + float m_colorGradingPostSaturation; + float m_colorGradingHueShift; +} + +#include + +struct PSOutput +{ + float4 m_lutOutput : SV_Target0; +}; + +PSOutput MainPS(VSOutput IN) +{ + ShaperType shaperType = (ShaperType)PassSrg::m_shaperType; + int lutResolution = PassSrg::m_lutResolution; + + PSOutput OUT; + + // baseCoords are from 0-1 + float3 baseCoords = convert2Dto3DLutCoords(IN.m_texCoord, lutResolution*lutResolution, lutResolution); + + float3 linearColor = ShaperToLinear(baseCoords, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale); + + linearColor = TransformColor(linearColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); + float3 gradedColor = ColorGrade(linearColor); + gradedColor = TransformColor(gradedColor, ColorSpaceId::ACEScg, ColorSpaceId::LinearSRGB); + + // Bring back coordinates into 0-1 + float3 shapedColor = LinearToShaper(gradedColor, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale); + shapedColor = saturate(shapedColor); + + OUT.m_lutOutput = float4(shapedColor, 1.0); + return OUT; +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader new file mode 100644 index 0000000000..4e2c83db35 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ColorGrading/LutGeneration.shader @@ -0,0 +1,22 @@ +{ + "Source" : "LutGeneration", + + "DepthStencilState" : { + "Depth" : { "Enable" : false } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl index cfa149f0d5..bc33a7e920 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/HDRColorGrading.azsl @@ -13,18 +13,6 @@ #include #include -#include -#include -#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli> -#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli> -#include <3rdParty/Features/PostProcessing/KelvinToRgb.azsli> - -static const float FloatEpsilon = 1.192092896e-07; // 1.0 + FloatEpsilon != 1.0, smallest positive float -static const float FloatMin = FLOAT_32_MIN; // Min float number that is positive -static const float FloatMax = FLOAT_32_MAX; // Max float number representable - -static const float AcesCcMidGrey = 0.4135884; - ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback { // get the framebuffer @@ -41,153 +29,42 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback AddressW = Clamp; }; + float m_colorAdjustmentWeight; float m_colorGradingExposure; float m_colorGradingContrast; - float m_colorGradingHueShift; float m_colorGradingPreSaturation; float m_colorFilterIntensity; float m_colorFilterMultiply; + float4 m_colorFilterSwatch; + + float m_whiteBalanceWeight; float m_whiteBalanceKelvin; float m_whiteBalanceTint; + float m_splitToneBalance; float m_splitToneWeight; - float m_colorGradingPostSaturation; + float4 m_splitToneShadowsColor; + float4 m_splitToneHighlightsColor; + float m_smhShadowsStart; float m_smhShadowsEnd; float m_smhHighlightsStart; float m_smhHighlightsEnd; float m_smhWeight; - - float3 m_channelMixingRed; - float3 m_channelMixingGreen; - float3 m_channelMixingBlue; - - float4 m_colorFilterSwatch; - float4 m_splitToneShadowsColor; - float4 m_splitToneHighlightsColor; - float4 m_smhShadowsColor; float4 m_smhMidtonesColor; float4 m_smhHighlightsColor; -} - -float SaturateWithEpsilon(float value) -{ - return clamp(value, FloatEpsilon, 1.0f); -} - -// Below are the color grading functions. These expect the frame color to be in ACEScg space. -// Note that some functions may have some quirks in their implementation and is subject to change. -float3 ColorGradePostExposure (float3 frameColor, float exposure) -{ - frameColor *= pow(2.0f, exposure); - return frameColor; -} - -// The contrast equation is performed in ACEScc (logarithmic) color space. -float3 ColorGradingContrast (float3 frameColor, float midgrey, float amount) -{ - const float contrastAdjustment = amount * 0.01f + 1.0f; - frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScg, ColorSpaceId::ACEScc); - frameColor = (frameColor - midgrey) * contrastAdjustment + midgrey; - return frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScc, ColorSpaceId::ACEScg); -} - -// The swatchColor param expects a linear RGB value. -float3 ColorGradeColorFilter (float3 frameColor, float3 swatchColor, float alpha) -{ - swatchColor = TransformColor(swatchColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); - swatchColor *= pow(2.0f, PassSrg::m_colorFilterIntensity); - const float3 frameAdjust = frameColor * swatchColor; - return frameColor = lerp(frameColor, frameAdjust, alpha); -} -float3 ColorGradeHueShift (float3 frameColor, float amount) -{ - float3 frameHsv = RgbToHsv(frameColor); - const float hue = frameHsv.x + amount; - frameHsv.x = RotateHue(hue, 0.0, 1.0); - return HsvToRgb(frameHsv); -} - -float3 ColorGradeSaturation (float3 frameColor, float control) -{ - const float vLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); - return (frameColor - vLuminance) * control + vLuminance; -} - -float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin) -{ - const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); - const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); - const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL - return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance -} - -// pow(f, e) won't work if f is negative, or may cause inf/NAN. -float3 NoNanPow(float3 base, float3 power) -{ - return pow(max(abs(base), float3(FloatEpsilon, FloatEpsilon, FloatEpsilon)), power); -} - -float3 ColorGradeSplitTone (float3 frameColor, float balance, float weight) -{ - float3 frameSplitTone = NoNanPow(frameColor, 1.0 / 2.2); - const float t = SaturateWithEpsilon(CalculateLuminance(SaturateWithEpsilon(frameSplitTone), ColorSpaceId::ACEScg) + balance); - const float3 shadows = lerp(0.5, PassSrg::m_splitToneShadowsColor.rgb, 1.0 - t); - const float3 highlights = lerp(0.5, PassSrg::m_splitToneHighlightsColor.rgb, t); - frameSplitTone = BlendMode_SoftLight(frameSplitTone, shadows); - frameSplitTone = BlendMode_SoftLight(frameSplitTone, highlights); - frameSplitTone = NoNanPow(frameSplitTone, 2.2); - return lerp(frameColor.rgb, frameSplitTone.rgb, weight); -} - -float3 ColorGradeChannelMixer (float3 frameColor) -{ - return mul(float3x3(PassSrg::m_channelMixingRed.rgb, - PassSrg::m_channelMixingGreen.rgb, - PassSrg::m_channelMixingBlue.rgb), - frameColor); -} + float3 m_channelMixingRed; + float3 m_channelMixingGreen; + float3 m_channelMixingBlue; -float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStart, float shadowsEnd, - float highlightsStart, float highlightsEnd, float weight, - float4 shadowsColor, float4 midtonesColor, float4 highlightsColor) -{ - const float3 shadowsColorACEScg = TransformColor(shadowsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); - const float3 midtonesColorACEScg = TransformColor(midtonesColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); - const float3 highlightsColorACEScg = TransformColor(highlightsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg); - - const float cLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg); - const float shadowsWeight = 1.0 - smoothstep(shadowsStart, shadowsEnd, cLuminance); - const float highlightsWeight = smoothstep(highlightsStart, highlightsEnd, cLuminance); - const float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight; - - const float3 frameSmh = frameColor * shadowsColorACEScg * shadowsWeight + - frameColor * midtonesColorACEScg * midtonesWeight + - frameColor * highlightsColorACEScg * highlightsWeight; - return lerp(frameColor.rgb, frameSmh.rgb, weight); + float m_finalAdjustmentWeight; + float m_colorGradingPostSaturation; + float m_colorGradingHueShift; } -float3 ColorGrade (float3 frameColor) -{ - frameColor = ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure); - frameColor = ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin); - frameColor = ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast); - frameColor = ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb, - PassSrg::m_colorFilterMultiply); - frameColor = max(frameColor, 0.0); - frameColor = ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation); - frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight); - frameColor = ColorGradeChannelMixer(frameColor); - frameColor = max(frameColor, 0.0); - frameColor = ColorGradeShadowsMidtonesHighlights(frameColor, PassSrg::m_smhShadowsStart, PassSrg::m_smhShadowsEnd, - PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight, - PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor); - frameColor = ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift); - frameColor = ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation); - return frameColor.rgb; -} +#include PSOutput MainPS(VSOutput IN) { diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake index cf456b2e41..123e5da7ba 100644 --- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake +++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake @@ -86,7 +86,6 @@ set(FILES Passes/CascadedShadowmaps.pass Passes/CheckerboardResolveColor.pass Passes/CheckerboardResolveDepth.pass - Passes/HDRColorGrading.pass Passes/ContrastAdaptiveSharpening.pass Passes/ConvertToAcescg.pass Passes/DebugOverlayParent.pass @@ -144,6 +143,7 @@ set(FILES Passes/ForwardSubsurfaceMSAA.pass Passes/FullscreenCopy.pass Passes/FullscreenOutputOnly.pass + Passes/HDRColorGrading.pass Passes/ImGui.pass Passes/KawaseShadowBlur.pass Passes/LightAdaptationParent.pass @@ -158,6 +158,7 @@ set(FILES Passes/LowEndPipeline.pass Passes/LuminanceHeatmap.pass Passes/LuminanceHistogramGenerator.pass + Passes/LutGeneration.pass Passes/MainPipeline.pass Passes/MainPipelineRenderToTexture.pass Passes/ThumbnailPipeline.pass @@ -281,6 +282,7 @@ set(FILES ShaderLib/Atom/Features/PostProcessing/FullscreenVertexUtil.azsli ShaderLib/Atom/Features/PostProcessing/GlyphData.azsli ShaderLib/Atom/Features/PostProcessing/GlyphRender.azsli + ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl ShaderLib/Atom/Features/PostProcessing/PostProcessUtil.azsli ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli ShaderLib/Atom/Features/ScreenSpace/ScreenSpaceUtil.azsli @@ -315,6 +317,8 @@ set(FILES Shaders/BRDFTexture/BRDFTextureCS.shader Shaders/Checkerboard/CheckerboardColorResolveCS.azsl Shaders/Checkerboard/CheckerboardColorResolveCS.shader + Shaders/ColorGrading/LutGeneration.azsl + Shaders/ColorGrading/LutGeneration.shader Shaders/Depth/DepthPass.azsl Shaders/Depth/DepthPass.shader Shaders/Depth/DepthPassTransparentMax.shader diff --git a/Gems/Atom/Feature/Common/Code/CMakeLists.txt b/Gems/Atom/Feature/Common/Code/CMakeLists.txt index 9c4ffe5b5e..b558be3714 100644 --- a/Gems/Atom/Feature/Common/Code/CMakeLists.txt +++ b/Gems/Atom/Feature/Common/Code/CMakeLists.txt @@ -39,6 +39,7 @@ ly_add_target( Gem::Atom_Utils.Static Gem::Atom_Feature_Common.Public Gem::ImGui.imguilib + 3rdParty::TIFF #3rdParty::lux_core # AZ_TRAIT_LUXCORE_SUPPORTED is disabled in every platform, Issue #3915 will remove RUNTIME_DEPENDENCIES Gem::ImGui.imguilib @@ -75,6 +76,7 @@ ly_add_target( FILES_CMAKE atom_feature_common_shared_files.cmake ../Assets/atom_feature_common_asset_files.cmake + ../Editor/atom_feature_common_editor_script_files.cmake PLATFORM_INCLUDE_FILES ${pal_source_dir}/runtime_dependencies_clients.cmake INCLUDE_DIRECTORIES diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ColorGrading/LutResolution.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ColorGrading/LutResolution.h new file mode 100644 index 0000000000..a33b49b419 --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ColorGrading/LutResolution.h @@ -0,0 +1,22 @@ +/* + * 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 + + namespace AZ +{ + namespace Render + { + enum class LutResolution + { + Lut16x16x16 = 16, + Lut32x32x32 = 32, + Lut64x64x64 = 64 + }; + } +} diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl index 241ba3b5de..c5865b0cfe 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingParams.inl @@ -10,28 +10,42 @@ // PARAM(NAME, MEMBER_NAME, DEFAULT_VALUE, ...) AZ_GFX_BOOL_PARAM(Enabled, m_enabled, false) +AZ_GFX_BOOL_PARAM(GenerateLut, m_generateLut, false) +AZ_GFX_FLOAT_PARAM(ColorAdjustmentWeight, m_colorAdjustmentWeight, 1.0) AZ_GFX_FLOAT_PARAM(ColorGradingExposure, m_colorGradingExposure, 0.0) AZ_GFX_FLOAT_PARAM(ColorGradingContrast, m_colorGradingContrast, 0.0) -AZ_GFX_FLOAT_PARAM(ColorGradingHueShift, m_colorGradingHueShift, 0.0) -AZ_GFX_FLOAT_PARAM(ColorGradingPreSaturation, m_colorGradingPreSaturation, 1.0) +AZ_GFX_FLOAT_PARAM(ColorGradingPreSaturation, m_colorGradingPreSaturation, 0.0) AZ_GFX_FLOAT_PARAM(ColorGradingFilterIntensity, m_colorGradingFilterIntensity, 1.0) AZ_GFX_FLOAT_PARAM(ColorGradingFilterMultiply, m_colorGradingFilterMultiply, 0.0) -AZ_GFX_FLOAT_PARAM(ColorGradingPostSaturation, m_colorGradingPostSaturation, 1.0) +AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f, 0.5f)) + +AZ_GFX_FLOAT_PARAM(WhiteBalanceWeight, m_whiteBalanceWeight, 0.0) AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0) AZ_GFX_FLOAT_PARAM(WhiteBalanceTint, m_whiteBalanceTint, 0.0) -AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0) + AZ_GFX_FLOAT_PARAM(SplitToneWeight, m_splitToneWeight, 0.0) +AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0) +AZ_GFX_VEC3_PARAM(SplitToneShadowsColor, m_splitToneShadowsColor, AZ::Vector3(1.0f, 0.5f, 0.5f)) +AZ_GFX_VEC3_PARAM(SplitToneHighlightsColor, m_splitToneHighlightsColor, AZ::Vector3(0.1f, 1.0f, 0.1f)) + +AZ_GFX_FLOAT_PARAM(SmhWeight, m_smhWeight, 0.0) AZ_GFX_FLOAT_PARAM(SmhShadowsStart, m_smhShadowsStart, 0.0) AZ_GFX_FLOAT_PARAM(SmhShadowsEnd, m_smhShadowsEnd, 0.3) AZ_GFX_FLOAT_PARAM(SmhHighlightsStart, m_smhHighlightsStart, 0.55) AZ_GFX_FLOAT_PARAM(SmhHighlightsEnd, m_smhHighlightsEnd, 1.0) -AZ_GFX_FLOAT_PARAM(SmhWeight, m_smhWeight, 0.0) -AZ_GFX_VEC3_PARAM(ChannelMixingRed, m_channelMixingRed, AZ::Vector3(1.0f, 0.0f, 0.0f)) -AZ_GFX_VEC3_PARAM(ChannelMixingGreen, m_channelMixingGreen, AZ::Vector3(0.0f, 1.0f, 0.0f)) -AZ_GFX_VEC3_PARAM(ChannelMixingBlue, m_channelMixingBlue, AZ::Vector3(0.0f, 0.f, 1.0f)) -AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f, 0.5f)) -AZ_GFX_VEC3_PARAM(SplitToneShadowsColor, m_splitToneShadowsColor, AZ::Vector3(1.0f, 0.5f, 0.5f)) -AZ_GFX_VEC3_PARAM(SplitToneHighlightsColor, m_splitToneHighlightsColor, AZ::Vector3(0.1f, 1.0f, 0.1f)) AZ_GFX_VEC3_PARAM(SmhShadowsColor, m_smhShadowsColor, AZ::Vector3(1.0f, 0.25f, 0.25f)) AZ_GFX_VEC3_PARAM(SmhMidtonesColor, m_smhMidtonesColor, AZ::Vector3(0.1f, 0.1f, 1.0f)) AZ_GFX_VEC3_PARAM(SmhHighlightsColor, m_smhHighlightsColor, AZ::Vector3(1.0f, 0.0f, 1.0f)) + +AZ_GFX_VEC3_PARAM(ChannelMixingRed, m_channelMixingRed, AZ::Vector3(1.0f, 0.0f, 0.0f)) +AZ_GFX_VEC3_PARAM(ChannelMixingGreen, m_channelMixingGreen, AZ::Vector3(0.0f, 1.0f, 0.0f)) +AZ_GFX_VEC3_PARAM(ChannelMixingBlue, m_channelMixingBlue, AZ::Vector3(0.0f, 0.f, 1.0f)) + +AZ_GFX_COMMON_PARAM(AZ::Render::LutResolution, LutResolution, m_lutResolution, AZ::Render::LutResolution::Lut16x16x16) +AZ_GFX_COMMON_PARAM(AZ::Render::ShaperPresetType, ShaperPresetType, m_shaperPresetType, AZ::Render::ShaperPresetType::None) +AZ_GFX_COMMON_PARAM(float, CustomMinExposure, m_customMinExposure, -6.5) +AZ_GFX_COMMON_PARAM(float, CustomMaxExposure, m_customMaxExposure, 6.5) + +AZ_GFX_FLOAT_PARAM(FinalAdjustmentWeight, m_finalAdjustmentWeight, 1.0) +AZ_GFX_FLOAT_PARAM(ColorGradingPostSaturation, m_colorGradingPostSaturation, 0.0) +AZ_GFX_FLOAT_PARAM(ColorGradingHueShift, m_colorGradingHueShift, 0.0) diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingSettingsInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingSettingsInterface.h index 019df206eb..62361b49c1 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingSettingsInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/PostProcess/ColorGrading/HDRColorGradingSettingsInterface.h @@ -12,6 +12,8 @@ #include #include #include +#include +#include namespace AZ { diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h index b22e82f456..707e3579a0 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ProfilingCaptureBus.h @@ -28,15 +28,6 @@ namespace AZ //! Dump the PipelineStatistics from passes to a json file. virtual bool CapturePassPipelineStatistics(const AZStd::string& outputFilePath) = 0; - //! Dump a single frame of Cpu profiling data - virtual bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) = 0; - - //! Start a multiframe capture of CPU profiling data. - virtual bool BeginContinuousCpuProfilingCapture() = 0; - - //! End and dump an in-progress continuous capture. - virtual bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) = 0; - //! Dump the benchmark metadata to a json file. virtual bool CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) = 0; }; @@ -63,11 +54,6 @@ namespace AZ //! @param info The output file path or error information which depends on the return. virtual void OnCaptureQueryPipelineStatisticsFinished(bool result, const AZStd::string& info) = 0; - //! Notify when the current CpuProfilingStatistics capture is finished - //! @param result Set to true if it's finished successfully - //! @param info The output file path or error information which depends on the return. - virtual void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) = 0; - //! Notify when the current BenchmarkMetadata capture is finished //! @param result Set to true if it's finished successfully //! @param info The output file path or error information which depends on the return. diff --git a/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.cpp b/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.cpp new file mode 100644 index 0000000000..edf26c3eec --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.cpp @@ -0,0 +1,90 @@ +/* + * 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 + +namespace AZ +{ + namespace Render + { + + RPI::Ptr LutGenerationPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew LutGenerationPass(descriptor); + return AZStd::move(pass); + } + + LutGenerationPass::LutGenerationPass(const RPI::PassDescriptor& descriptor) + : HDRColorGradingPass(descriptor) + { + } + + void LutGenerationPass::InitializeInternal() + { + HDRColorGradingPass::InitializeInternal(); + + m_lutResolutionIndex.Reset(); + m_lutShaperTypeIndex.Reset(); + m_lutShaperScaleIndex.Reset(); + } + + void LutGenerationPass::FrameBeginInternal(FramePrepareParams params) + { + const auto* colorGradingSettings = GetHDRColorGradingSettings(); + if (colorGradingSettings) + { + m_shaderResourceGroup->SetConstant(m_lutResolutionIndex, colorGradingSettings->GetLutResolution()); + + auto shaperParams = AcesDisplayMapperFeatureProcessor::GetShaperParameters( + colorGradingSettings->GetShaperPresetType(), + colorGradingSettings->GetCustomMinExposure(), + colorGradingSettings->GetCustomMaxExposure()); + m_shaderResourceGroup->SetConstant(m_lutShaperTypeIndex, shaperParams.m_type); + m_shaderResourceGroup->SetConstant(m_lutShaperBiasIndex, shaperParams.m_bias); + m_shaderResourceGroup->SetConstant(m_lutShaperScaleIndex, shaperParams.m_scale); + } + + HDRColorGradingPass::FrameBeginInternal(params); + } + + void LutGenerationPass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) + { + const auto* colorGradingSettings = GetHDRColorGradingSettings(); + if (colorGradingSettings) + { + uint32_t lutResolution = aznumeric_cast(colorGradingSettings->GetLutResolution()); + RHI::Size lutSize{ lutResolution * lutResolution, lutResolution, 1 }; + + RPI::Ptr attachment = FindOwnedAttachment(Name{ "ColorGradingLut" }); + RHI::ImageDescriptor& imageDescriptor = attachment->m_descriptor.m_image; + imageDescriptor.m_size = lutSize; + SetViewportScissorFromImageSize(lutSize); + } + HDRColorGradingPass::BuildCommandListInternal(context); + } + + bool LutGenerationPass::IsEnabled() const + { + const auto* colorGradingSettings = GetHDRColorGradingSettings(); + return colorGradingSettings ? colorGradingSettings->GetGenerateLut() : false; + } + + void LutGenerationPass::SetViewportScissorFromImageSize(const RHI::Size& imageSize) + { + const RHI::Viewport viewport(0.f, imageSize.m_width * 1.f, 0.f, imageSize.m_height * 1.f); + const RHI::Scissor scissor(0, 0, imageSize.m_width, imageSize.m_height); + m_viewportState = viewport; + m_scissorState = scissor; + } + + } // namespace Render +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.h b/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.h new file mode 100644 index 0000000000..4a9b358d2f --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/ColorGrading/LutGenerationPass.h @@ -0,0 +1,54 @@ +/* + * 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 + +namespace AZ +{ + namespace Render + { + // Performs color grading on an identity LUT strip + class LutGenerationPass + : public AZ::Render::HDRColorGradingPass + { + public: + AZ_RTTI(LutGenerationPass, "{C21DABA8-B538-4C80-BA18-5B97CC9259E5}", AZ::RPI::FullscreenTrianglePass); + AZ_CLASS_ALLOCATOR(LutGenerationPass, SystemAllocator, 0); + + virtual ~LutGenerationPass() = default; + + //! Creates a ColorGradingPass + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + LutGenerationPass(const RPI::PassDescriptor& descriptor); + + void InitializeInternal() override; + void FrameBeginInternal(FramePrepareParams params) override; + void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override; + bool IsEnabled() const override; + private: + // Set viewport scissor based on output LUT resolution + void SetViewportScissorFromImageSize(const RHI::Size& imageSize); + + RHI::ShaderInputNameIndex m_lutResolutionIndex = "m_lutResolution"; + RHI::ShaderInputNameIndex m_lutShaperTypeIndex = "m_shaperType"; + RHI::ShaderInputNameIndex m_lutShaperBiasIndex = "m_shaperBias"; + RHI::ShaderInputNameIndex m_lutShaperScaleIndex = "m_shaperScale"; + }; + + } // namespace Render +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp index 22c5f37ff8..4ddf609f41 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -230,6 +231,7 @@ namespace AZ passSystem->AddPassCreator(Name("HDRColorGradingPass"), &HDRColorGradingPass::Create); passSystem->AddPassCreator(Name("LookModificationCompositePass"), &LookModificationCompositePass::Create); passSystem->AddPassCreator(Name("LookModificationTransformPass"), &LookModificationPass::Create); + passSystem->AddPassCreator(Name("LutGenerationPass"), &LutGenerationPass::Create); passSystem->AddPassCreator(Name("SMAAEdgeDetectionPass"), &SMAAEdgeDetectionPass::Create); passSystem->AddPassCreator(Name("SMAABlendingWeightCalculationPass"), &SMAABlendingWeightCalculationPass::Create); passSystem->AddPassCreator(Name("SMAANeighborhoodBlendingPass"), &SMAANeighborhoodBlendingPass::Create); diff --git a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp index cdcc07b545..8c0360dec2 100644 --- a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp @@ -35,6 +35,7 @@ #include #include +#include namespace AZ { namespace Render @@ -110,6 +111,48 @@ namespace AZ return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to '" + outputFilePath + "'"}; } + FrameCaptureOutputResult TiffFrameCaptureOutput( + const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) + { + AZStd::shared_ptr> buffer = readbackResult.m_dataBuffer; + const uint32_t width = readbackResult.m_imageDescriptor.m_size.m_width; + const uint32_t height = readbackResult.m_imageDescriptor.m_size.m_height; + const uint32_t numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format); + const uint32_t bytesPerChannel = AZ::RHI::GetFormatSize(readbackResult.m_imageDescriptor.m_format) / numChannels; + const uint32_t bitsPerChannel = bytesPerChannel * 8; + + TIFF* out = TIFFOpen(outputFilePath.c_str(), "w"); + TIFFSetField(out, TIFFTAG_IMAGEWIDTH, width); + TIFFSetField(out, TIFFTAG_IMAGELENGTH, height); + TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, numChannels); + TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bitsPerChannel); + TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); // interpret each pixel as a float + + size_t pitch = width * numChannels * bytesPerChannel; + AZ_Assert((pitch * height) == buffer->size(), "Image buffer does not match allocated bytes for tiff saving.") + unsigned char* raster = (unsigned char*)_TIFFmalloc((tsize_t)(pitch * height)); + memcpy(raster, buffer->data(), pitch * height); + bool success = true; + for (uint32_t h = 0; h < height; ++h) + { + size_t offset = h * pitch; + int err = TIFFWriteScanline(out, raster + offset, h, 0); + if (err < 0) + { + success = false; + break; + } + } + _TIFFfree(raster); + TIFFClose(out); + return success ? FrameCaptureOutputResult{ FrameCaptureResult::Success, AZStd::nullopt } + : FrameCaptureOutputResult{ FrameCaptureResult::InternalError, "Unable to save tif frame capture output to " + outputFilePath }; + } + FrameCaptureOutputResult DdsFrameCaptureOutput( const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) { @@ -492,6 +535,12 @@ namespace AZ m_result = ddsFrameCapture.m_result; m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or(""); } + else if (extension == "tiff" || extension == "tif") + { + const auto tifFrameCapture = TiffFrameCaptureOutput(m_outputFilePath, readbackResult); + m_result = tifFrameCapture.m_result; + m_latestCaptureInfo = tifFrameCapture.m_errorMessage.value_or(""); + } else if (extension == "png") { if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM || diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.cpp index 4542b53a15..bc23ea3769 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.cpp @@ -33,6 +33,7 @@ namespace AZ target->m_enabled = m_enabled; #define AZ_GFX_BOOL_PARAM(NAME, MEMBER_NAME, DefaultValue) ; +#define AZ_GFX_COMMON_PARAM(ValueType, Name, MemberName, DefaultValue) ; #define AZ_GFX_FLOAT_PARAM(NAME, MEMBER_NAME, DefaultValue) \ { \ target->Set##NAME(AZ::Lerp(target->MEMBER_NAME, MEMBER_NAME, alpha)); \ diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.h b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.h index 2358919c28..4f00192e9e 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ColorGrading/HDRColorGradingSettings.h @@ -13,6 +13,7 @@ #include #include +#include namespace AZ { diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp index 62fff6b6fe..88266ba789 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.cpp @@ -32,33 +32,39 @@ { FullscreenTrianglePass::InitializeInternal(); + m_colorAdjustmentWeightIndex.Reset(); m_colorGradingExposureIndex.Reset(); m_colorGradingContrastIndex.Reset(); - m_colorGradingHueShiftIndex.Reset(); m_colorGradingPreSaturationIndex.Reset(); m_colorFilterIntensityIndex.Reset(); m_colorFilterMultiplyIndex.Reset(); + m_colorFilterSwatchIndex.Reset(); + + m_whiteBalanceWeightIndex.Reset(); m_whiteBalanceKelvinIndex.Reset(); m_whiteBalanceTintIndex.Reset(); + m_splitToneBalanceIndex.Reset(); m_splitToneWeightIndex.Reset(); - m_colorGradingPostSaturationIndex.Reset(); + m_splitToneShadowsColorIndex.Reset(); + m_splitToneHighlightsColorIndex.Reset(); + m_smhShadowsStartIndex.Reset(); m_smhShadowsEndIndex.Reset(); m_smhHighlightsStartIndex.Reset(); m_smhHighlightsEndIndex.Reset(); m_smhWeightIndex.Reset(); + m_smhShadowsColorIndex.Reset(); + m_smhMidtonesColorIndex.Reset(); + m_smhHighlightsColorIndex.Reset(); m_channelMixingRedIndex.Reset(); m_channelMixingGreenIndex.Reset(); m_channelMixingBlueIndex.Reset(); - m_colorFilterSwatchIndex.Reset(); - m_splitToneShadowsColorIndex.Reset(); - m_splitToneHighlightsColorIndex.Reset(); - m_smhShadowsColorIndex.Reset(); - m_smhMidtonesColorIndex.Reset(); - m_smhHighlightsColorIndex.Reset(); + m_finalAdjustmentWeightIndex.Reset(); + m_colorGradingPostSaturationIndex.Reset(); + m_colorGradingHueShiftIndex.Reset(); } void HDRColorGradingPass::FrameBeginInternal(FramePrepareParams params) @@ -79,33 +85,39 @@ const HDRColorGradingSettings* settings = GetHDRColorGradingSettings(); if (settings) { + m_shaderResourceGroup->SetConstant(m_colorAdjustmentWeightIndex, settings->GetColorAdjustmentWeight()); m_shaderResourceGroup->SetConstant(m_colorGradingExposureIndex, settings->GetColorGradingExposure()); m_shaderResourceGroup->SetConstant(m_colorGradingContrastIndex, settings->GetColorGradingContrast()); - m_shaderResourceGroup->SetConstant(m_colorGradingHueShiftIndex, settings->GetColorGradingHueShift()); - m_shaderResourceGroup->SetConstant(m_colorGradingPreSaturationIndex, settings->GetColorGradingPreSaturation()); + m_shaderResourceGroup->SetConstant(m_colorGradingPreSaturationIndex, settings->GetColorGradingPreSaturation() * 0.01f + 1.0f); m_shaderResourceGroup->SetConstant(m_colorFilterIntensityIndex, settings->GetColorGradingFilterIntensity()); m_shaderResourceGroup->SetConstant(m_colorFilterMultiplyIndex, settings->GetColorGradingFilterMultiply()); + m_shaderResourceGroup->SetConstant(m_colorFilterSwatchIndex, AZ::Vector4(settings->GetColorFilterSwatch())); + + m_shaderResourceGroup->SetConstant(m_whiteBalanceWeightIndex, settings->GetWhiteBalanceWeight()); m_shaderResourceGroup->SetConstant(m_whiteBalanceKelvinIndex, settings->GetWhiteBalanceKelvin()); m_shaderResourceGroup->SetConstant(m_whiteBalanceTintIndex, settings->GetWhiteBalanceTint()); + m_shaderResourceGroup->SetConstant(m_splitToneBalanceIndex, settings->GetSplitToneBalance()); m_shaderResourceGroup->SetConstant(m_splitToneWeightIndex, settings->GetSplitToneWeight()); - m_shaderResourceGroup->SetConstant(m_colorGradingPostSaturationIndex, settings->GetColorGradingPostSaturation()); + m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor())); + m_shaderResourceGroup->SetConstant(m_splitToneHighlightsColorIndex, AZ::Vector4(settings->GetSplitToneHighlightsColor())); + m_shaderResourceGroup->SetConstant(m_smhShadowsStartIndex, settings->GetSmhShadowsStart()); m_shaderResourceGroup->SetConstant(m_smhShadowsEndIndex, settings->GetSmhShadowsEnd()); m_shaderResourceGroup->SetConstant(m_smhHighlightsStartIndex, settings->GetSmhHighlightsStart()); m_shaderResourceGroup->SetConstant(m_smhHighlightsEndIndex, settings->GetSmhHighlightsEnd()); m_shaderResourceGroup->SetConstant(m_smhWeightIndex, settings->GetSmhWeight()); + m_shaderResourceGroup->SetConstant(m_smhShadowsColorIndex, AZ::Vector4(settings->GetSmhShadowsColor())); + m_shaderResourceGroup->SetConstant(m_smhMidtonesColorIndex, AZ::Vector4(settings->GetSmhMidtonesColor())); + m_shaderResourceGroup->SetConstant(m_smhHighlightsColorIndex, AZ::Vector4(settings->GetSmhHighlightsColor())); m_shaderResourceGroup->SetConstant(m_channelMixingRedIndex, settings->GetChannelMixingRed()); m_shaderResourceGroup->SetConstant(m_channelMixingGreenIndex, settings->GetChannelMixingGreen()); m_shaderResourceGroup->SetConstant(m_channelMixingBlueIndex, settings->GetChannelMixingBlue()); - m_shaderResourceGroup->SetConstant(m_colorFilterSwatchIndex, AZ::Vector4(settings->GetColorFilterSwatch())); - m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor())); - m_shaderResourceGroup->SetConstant(m_splitToneHighlightsColorIndex, AZ::Vector4(settings->GetSplitToneHighlightsColor())); - m_shaderResourceGroup->SetConstant(m_smhShadowsColorIndex, AZ::Vector4(settings->GetSmhShadowsColor())); - m_shaderResourceGroup->SetConstant(m_smhMidtonesColorIndex, AZ::Vector4(settings->GetSmhMidtonesColor())); - m_shaderResourceGroup->SetConstant(m_smhHighlightsColorIndex, AZ::Vector4(settings->GetSmhHighlightsColor())); + m_shaderResourceGroup->SetConstant(m_finalAdjustmentWeightIndex, settings->GetFinalAdjustmentWeight()); + m_shaderResourceGroup->SetConstant(m_colorGradingPostSaturationIndex, settings->GetColorGradingPostSaturation() * 0.01f + 1.0f); + m_shaderResourceGroup->SetConstant(m_colorGradingHueShiftIndex, settings->GetColorGradingHueShift()); } } diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h index e5eaf7db26..dc706b8501 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/HDRColorGradingPass.h @@ -41,37 +41,42 @@ namespace AZ void FrameBeginInternal(FramePrepareParams params) override; bool IsEnabled() const override; - private: const HDRColorGradingSettings* GetHDRColorGradingSettings() const; - void SetSrgConstants(); - + virtual void SetSrgConstants(); + private: + RHI::ShaderInputNameIndex m_colorAdjustmentWeightIndex = "m_colorAdjustmentWeight"; RHI::ShaderInputNameIndex m_colorGradingExposureIndex = "m_colorGradingExposure"; RHI::ShaderInputNameIndex m_colorGradingContrastIndex = "m_colorGradingContrast"; - RHI::ShaderInputNameIndex m_colorGradingHueShiftIndex = "m_colorGradingHueShift"; RHI::ShaderInputNameIndex m_colorGradingPreSaturationIndex = "m_colorGradingPreSaturation"; RHI::ShaderInputNameIndex m_colorFilterIntensityIndex = "m_colorFilterIntensity"; RHI::ShaderInputNameIndex m_colorFilterMultiplyIndex = "m_colorFilterMultiply"; + RHI::ShaderInputNameIndex m_colorFilterSwatchIndex = "m_colorFilterSwatch"; + + RHI::ShaderInputNameIndex m_whiteBalanceWeightIndex = "m_whiteBalanceWeight"; RHI::ShaderInputNameIndex m_whiteBalanceKelvinIndex = "m_whiteBalanceKelvin"; RHI::ShaderInputNameIndex m_whiteBalanceTintIndex = "m_whiteBalanceTint"; + RHI::ShaderInputNameIndex m_splitToneBalanceIndex = "m_splitToneBalance"; RHI::ShaderInputNameIndex m_splitToneWeightIndex = "m_splitToneWeight"; - RHI::ShaderInputNameIndex m_colorGradingPostSaturationIndex = "m_colorGradingPostSaturation"; + RHI::ShaderInputNameIndex m_splitToneShadowsColorIndex = "m_splitToneShadowsColor"; + RHI::ShaderInputNameIndex m_splitToneHighlightsColorIndex = "m_splitToneHighlightsColor"; + RHI::ShaderInputNameIndex m_smhShadowsStartIndex = "m_smhShadowsStart"; RHI::ShaderInputNameIndex m_smhShadowsEndIndex = "m_smhShadowsEnd"; RHI::ShaderInputNameIndex m_smhHighlightsStartIndex = "m_smhHighlightsStart"; RHI::ShaderInputNameIndex m_smhHighlightsEndIndex = "m_smhHighlightsEnd"; RHI::ShaderInputNameIndex m_smhWeightIndex = "m_smhWeight"; + RHI::ShaderInputNameIndex m_smhShadowsColorIndex = "m_smhShadowsColor"; + RHI::ShaderInputNameIndex m_smhMidtonesColorIndex = "m_smhMidtonesColor"; + RHI::ShaderInputNameIndex m_smhHighlightsColorIndex = "m_smhHighlightsColor"; RHI::ShaderInputNameIndex m_channelMixingRedIndex = "m_channelMixingRed"; RHI::ShaderInputNameIndex m_channelMixingGreenIndex = "m_channelMixingGreen"; RHI::ShaderInputNameIndex m_channelMixingBlueIndex = "m_channelMixingBlue"; - RHI::ShaderInputNameIndex m_colorFilterSwatchIndex = "m_colorFilterSwatch"; - RHI::ShaderInputNameIndex m_splitToneShadowsColorIndex = "m_splitToneShadowsColor"; - RHI::ShaderInputNameIndex m_splitToneHighlightsColorIndex = "m_splitToneHighlightsColor"; - RHI::ShaderInputNameIndex m_smhShadowsColorIndex = "m_smhShadowsColor"; - RHI::ShaderInputNameIndex m_smhMidtonesColorIndex = "m_smhMidtonesColor"; - RHI::ShaderInputNameIndex m_smhHighlightsColorIndex = "m_smhHighlightsColor"; + RHI::ShaderInputNameIndex m_finalAdjustmentWeightIndex = "m_finalAdjustmentWeight"; + RHI::ShaderInputNameIndex m_colorGradingPostSaturationIndex = "m_colorGradingPostSaturation"; + RHI::ShaderInputNameIndex m_colorGradingHueShiftIndex = "m_colorGradingHueShift"; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp index 4accbf0bba..66adfe9985 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.cpp @@ -8,7 +8,6 @@ #include "ProfilingCaptureSystemComponent.h" -#include #include #include #include @@ -39,7 +38,6 @@ namespace AZ OnCaptureQueryTimestampFinished, OnCaptureCpuFrameTimeFinished, OnCaptureQueryPipelineStatisticsFinished, - OnCaptureCpuProfilingStatisticsFinished, OnCaptureBenchmarkMetadataFinished ); @@ -58,11 +56,6 @@ namespace AZ Call(FN_OnCaptureQueryPipelineStatisticsFinished, result, info); } - void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) override - { - Call(FN_OnCaptureCpuProfilingStatisticsFinished, result, info); - } - void OnCaptureBenchmarkMetadataFinished(bool result, const AZStd::string& info) override { Call(FN_OnCaptureBenchmarkMetadataFinished, result, info); @@ -358,7 +351,6 @@ namespace AZ ->Event("CapturePassTimestamp", &ProfilingCaptureRequestBus::Events::CapturePassTimestamp) ->Event("CaptureCpuFrameTime", &ProfilingCaptureRequestBus::Events::CaptureCpuFrameTime) ->Event("CapturePassPipelineStatistics", &ProfilingCaptureRequestBus::Events::CapturePassPipelineStatistics) - ->Event("CaptureCpuProfilingStatistics", &ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics) ->Event("CaptureBenchmarkMetadata", &ProfilingCaptureRequestBus::Events::CaptureBenchmarkMetadata) ; @@ -368,7 +360,6 @@ namespace AZ TimestampSerializer::Reflect(context); CpuFrameTimeSerializer::Reflect(context); PipelineStatisticsSerializer::Reflect(context); - RHI::CpuProfilingStatisticsSerializer::Reflect(context); BenchmarkMetadataSerializer::Reflect(context); } @@ -382,12 +373,6 @@ namespace AZ TickBus::Handler::BusDisconnect(); ProfilingCaptureRequestBus::Handler::BusDisconnect(); - - // Block deactivation until the IO thread has finished serializing the CPU data - if (m_cpuDataSerializationThread.joinable()) - { - m_cpuDataSerializationThread.join(); - } } bool ProfilingCaptureSystemComponent::CapturePassTimestamp(const AZStd::string& outputFilePath) @@ -442,16 +427,7 @@ namespace AZ bool ProfilingCaptureSystemComponent::CaptureCpuFrameTime(const AZStd::string& outputFilePath) { - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, true - ); - bool wasEnabled = RHI::CpuProfiler::Get()->IsProfilerEnabled(); - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(true); - } - - const bool captureStarted = m_cpuFrameTimeStatisticsCapture.StartCapture([outputFilePath, wasEnabled]() + const bool captureStarted = m_cpuFrameTimeStatisticsCapture.StartCapture([outputFilePath]() { JsonSerializerSettings serializationSettings; serializationSettings.m_keepDefaults = true; @@ -472,15 +448,6 @@ namespace AZ AZ_Warning("ProfilingCaptureSystemComponent", false, captureInfo.c_str()); } - // Disable the profiler again - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(false); - } - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, false - ); - // Notify listeners that the Cpu frame time statistics capture has finished. ProfilingCaptureNotificationBus::Broadcast(&ProfilingCaptureNotificationBus::Events::OnCaptureCpuFrameTimeFinished, saveResult.IsSuccess(), @@ -546,116 +513,6 @@ namespace AZ return captureStarted; } - bool SerializeCpuProfilingData(const AZStd::ring_buffer& data, AZStd::string outputFilePath, bool wasEnabled) - { - AZ_TracePrintf("ProfilingCaptureSystemComponent", "Beginning serialization of %zu frames of profiling data\n", data.size()); - JsonSerializerSettings serializationSettings; - serializationSettings.m_keepDefaults = true; - - RHI::CpuProfilingStatisticsSerializer serializer(data); - - const auto saveResult = JsonSerializationUtils::SaveObjectToFile(&serializer, - outputFilePath, (RHI::CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings); - - AZStd::string captureInfo = outputFilePath; - if (!saveResult.IsSuccess()) - { - captureInfo = AZStd::string::format("Failed to save Cpu Profiling Statistics data to file '%s'. Error: %s", - outputFilePath.c_str(), - saveResult.GetError().c_str()); - AZ_Warning("ProfilingCaptureSystemComponent", false, captureInfo.c_str()); - } - else - { - AZ_Printf("ProfilingCaptureSystemComponent", "Cpu profiling statistics was saved to file [%s]\n", outputFilePath.c_str()); - } - - // Disable the profiler again - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(false); - } - - // Notify listeners that the pass' PipelineStatistics queries capture has finished. - ProfilingCaptureNotificationBus::Broadcast(&ProfilingCaptureNotificationBus::Events::OnCaptureCpuProfilingStatisticsFinished, - saveResult.IsSuccess(), - captureInfo); - return saveResult.IsSuccess(); - } - - bool ProfilingCaptureSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) - { - // Start the cpu profiling - bool wasEnabled = RHI::CpuProfiler::Get()->IsProfilerEnabled(); - if (!wasEnabled) - { - RHI::CpuProfiler::Get()->SetProfilerEnabled(true); - } - - const bool captureStarted = m_cpuProfilingStatisticsCapture.StartCapture([outputFilePath, wasEnabled]() - { - // Blocking call for a single frame of data, avoid thread overhead - AZStd::ring_buffer singleFrameData(1); - singleFrameData.push_back(RHI::CpuProfiler::Get()->GetTimeRegionMap()); - SerializeCpuProfilingData(singleFrameData, outputFilePath, wasEnabled); - }); - - // Start the TickBus. - if (captureStarted) - { - TickBus::Handler::BusConnect(); - } - - return captureStarted; - } - - bool ProfilingCaptureSystemComponent::BeginContinuousCpuProfilingCapture() - { - return AZ::RHI::CpuProfiler::Get()->BeginContinuousCapture(); - } - - bool ProfilingCaptureSystemComponent::EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) - { - bool expected = false; - if (m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true)) - { - AZStd::ring_buffer captureResult; - const bool captureEnded = AZ::RHI::CpuProfiler::Get()->EndContinuousCapture(captureResult); - if (!captureEnded) - { - AZ_TracePrintf("ProfilingCaptureSystemComponent", "Could not end the continuous capture, is one in progress?\n"); - m_cpuDataSerializationInProgress.store(false); - return false; - } - - // cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk. - auto threadIoFunction = - [data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]() - { - SerializeCpuProfilingData(data, filePath, true); - flag.store(false); - }; - - // If the thread object already exists (ex. we have already serialized data), join. This will not block since - // m_cpuDataSerializationInProgress was false, meaning the IO thread has already completed execution. - // TODO Use a reusable thread implementation over repeated creation + destruction of threads [ATOM-16214] - if (m_cpuDataSerializationThread.joinable()) - { - m_cpuDataSerializationThread.join(); - } - - auto thread = AZStd::thread(threadIoFunction); - m_cpuDataSerializationThread = AZStd::move(thread); - - return true; - } - - AZ_TracePrintf( - "ProfilingSystemCaptureComponent", - "Cannot end a continuous capture - another serialization is currently in progress\n"); - return false; - } - bool ProfilingCaptureSystemComponent::CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) { const bool captureStarted = m_benchmarkMetadataCapture.StartCapture([benchmarkName, outputFilePath]() @@ -734,11 +591,10 @@ namespace AZ m_timestampCapture.UpdateCapture(); m_cpuFrameTimeStatisticsCapture.UpdateCapture(); m_pipelineStatisticsCapture.UpdateCapture(); - m_cpuProfilingStatisticsCapture.UpdateCapture(); m_benchmarkMetadataCapture.UpdateCapture(); // Disconnect from the TickBus if all capture states are set to idle. - if (m_timestampCapture.IsIdle() && m_pipelineStatisticsCapture.IsIdle() && m_cpuProfilingStatisticsCapture.IsIdle() && m_benchmarkMetadataCapture.IsIdle() && m_cpuFrameTimeStatisticsCapture.IsIdle()) + if (m_timestampCapture.IsIdle() && m_pipelineStatisticsCapture.IsIdle() && m_benchmarkMetadataCapture.IsIdle() && m_cpuFrameTimeStatisticsCapture.IsIdle()) { TickBus::Handler::BusDisconnect(); } diff --git a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h index 9f8a8a90c6..a9bb8c585f 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h +++ b/Gems/Atom/Feature/Common/Code/Source/ProfilingCaptureSystemComponent.h @@ -70,9 +70,6 @@ namespace AZ bool CapturePassTimestamp(const AZStd::string& outputFilePath) override; bool CaptureCpuFrameTime(const AZStd::string& outputFilePath) override; bool CapturePassPipelineStatistics(const AZStd::string& outputFilePath) override; - bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override; - bool BeginContinuousCpuProfilingCapture() override; - bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override; bool CaptureBenchmarkMetadata(const AZStd::string& benchmarkName, const AZStd::string& outputFilePath) override; private: @@ -86,13 +83,7 @@ namespace AZ DelayedQueryCaptureHelper m_timestampCapture; DelayedQueryCaptureHelper m_cpuFrameTimeStatisticsCapture; DelayedQueryCaptureHelper m_pipelineStatisticsCapture; - DelayedQueryCaptureHelper m_cpuProfilingStatisticsCapture; DelayedQueryCaptureHelper m_benchmarkMetadataCapture; - - // Flag passed by reference to the CPU profiling data serialization job, blocks new continuous capture requests when set. - AZStd::atomic_bool m_cpuDataSerializationInProgress = false; - - AZStd::thread m_cpuDataSerializationThread; }; } } diff --git a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake index 18cdc273d2..c875f7c177 100644 --- a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake +++ b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake @@ -12,6 +12,7 @@ set(FILES Include/Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h Include/Atom/Feature/Automation/AtomAutomationBus.h Include/Atom/Feature/AuxGeom/AuxGeomFeatureProcessor.h + Include/Atom/Feature/ColorGrading/LutResolution.h Include/Atom/Feature/CoreLights/CoreLightsConstants.h Include/Atom/Feature/DisplayMapper/AcesOutputTransformPass.h Include/Atom/Feature/DisplayMapper/AcesOutputTransformLutPass.h @@ -66,6 +67,8 @@ set(FILES Source/AuxGeom/DynamicPrimitiveProcessor.h Source/AuxGeom/FixedShapeProcessor.cpp Source/AuxGeom/FixedShapeProcessor.h + Source/ColorGrading/LutGenerationPass.cpp + Source/ColorGrading/LutGenerationPass.h Source/CoreLights/CapsuleLightFeatureProcessor.h Source/CoreLights/CapsuleLightFeatureProcessor.cpp Source/CoreLights/CascadedShadowmapsPass.h @@ -212,8 +215,6 @@ set(FILES Source/PostProcessing/BloomCompositePass.cpp Source/PostProcessing/BloomParentPass.h Source/PostProcessing/BloomParentPass.cpp - Source/PostProcessing/HDRColorGradingPass.cpp - Source/PostProcessing/HDRColorGradingPass.h Source/PostProcessing/DepthOfFieldCompositePass.h Source/PostProcessing/DepthOfFieldCompositePass.cpp Source/PostProcessing/DepthOfFieldBokehBlurPass.h @@ -234,6 +235,8 @@ set(FILES Source/PostProcessing/EyeAdaptationPass.h Source/PostProcessing/FastDepthAwareBlurPasses.cpp Source/PostProcessing/FastDepthAwareBlurPasses.h + Source/PostProcessing/HDRColorGradingPass.cpp + Source/PostProcessing/HDRColorGradingPass.h Source/PostProcessing/LookModificationCompositePass.cpp Source/PostProcessing/LookModificationCompositePass.h Source/PostProcessing/LookModificationTransformPass.cpp diff --git a/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/activate_lut_asset.py b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/activate_lut_asset.py new file mode 100644 index 0000000000..016ad024aa --- /dev/null +++ b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/activate_lut_asset.py @@ -0,0 +1,89 @@ +""" +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 +""" + +""" +This script is used by the Editor HDR Color Grading component to use a stored lut asset path +and pass it onto a Look Modification Component. + +The HDR Color Grading component will be disabled as it is not compatible with the Look +Modification Component. +""" +import azlmbr +import azlmbr.legacy.general as general + +LOOK_MODIFICATION_LUT_PROPERTY_PATH = 'Controller|Configuration|Color Grading LUT' +LOOK_MODIFICATION_ENABLE_PROPERTY_PATH = 'Controller|Configuration|Enable look modification' +COLOR_GRADING_COMPONENT_ID = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["HDR Color Grading"], 0) +LOOK_MODIFICATION_COMPONENT_ID = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'FindComponentTypeIdsByEntityType', ["Look Modification"], 0) + + +def disable_hdr_color_grading_component(entity_id): + componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', entity_id, COLOR_GRADING_COMPONENT_ID[0]) + if(componentOutcome.IsSuccess()): + azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'DisableComponents', [componentOutcome.GetValue()]) + +def add_look_modification_component(entity_id): + componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'AddComponentsOfType', entity_id, LOOK_MODIFICATION_COMPONENT_ID) + return componentOutcome.GetValue()[0] + +def get_look_modification_component(entity_id): + componentOutcome = azlmbr.editor.EditorComponentAPIBus(azlmbr.bus.Broadcast, 'GetComponentOfType', entity_id, LOOK_MODIFICATION_COMPONENT_ID[0]) + if componentOutcome.IsSuccess(): + return componentOutcome.GetValue() + else: + return None + +def activate_look_modification_lut(look_modification_component, asset_relative_path): + print(asset_relative_path) + asset_id = azlmbr.asset.AssetCatalogRequestBus( + azlmbr.bus.Broadcast, + 'GetAssetIdByPath', + asset_relative_path, + azlmbr.math.Uuid(), + False + ) + azlmbr.editor.EditorComponentAPIBus( + azlmbr.bus.Broadcast, + 'SetComponentProperty', + look_modification_component, + LOOK_MODIFICATION_LUT_PROPERTY_PATH, + asset_id + ) + azlmbr.editor.EditorComponentAPIBus( + azlmbr.bus.Broadcast, + 'SetComponentProperty', + look_modification_component, + LOOK_MODIFICATION_ENABLE_PROPERTY_PATH, + True + ) + +def activate_lut_asset(entity_id, asset_relative_path): + disable_hdr_color_grading_component(entity_id) + + look_modification_component = get_look_modification_component(entity_id) + if not look_modification_component: + look_modification_component = add_look_modification_component(entity_id) + + general.idle_wait_frames(5) + + if look_modification_component: + activate_look_modification_lut(look_modification_component, asset_relative_path) + + +if __name__ == "__main__": + parser=argparse.ArgumentParser() + parser.add_argument('--entityName', type=str, required=True, help='Entity ID to manage') + parser.add_argument('--assetRelativePath', type=str, required=True, help='Lut asset relative path to activate') + args=parser.parse_args() + + # Get the entity id + searchFilter = azlmbr.entity.SearchFilter() + searchFilter.names = [args.entityName] + entityIdList = azlmbr.entity.SearchBus(azlmbr.bus.Broadcast, 'SearchEntities', searchFilter) + + for entityId in entityIdList: + activate_lut_asset(entityId, args.assetRelativePath) diff --git a/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/azasset_converter_utils.py b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/azasset_converter_utils.py new file mode 100644 index 0000000000..1a2bf41711 --- /dev/null +++ b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/azasset_converter_utils.py @@ -0,0 +1,63 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +import numpy as np +import logging as _logging +from ColorGrading import get_uv_coord + +# ------------------------------------------------------------------------ +_MODULENAME = 'ColorGrading.azasset_converter_utils' + +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------ + +""" Utility functions for generating LUT azassets """ +def generate_lut_values(image_spec, image_buffer): + lut_size = image_spec.height + + lut_intervals = [] + lut_values = [] + + # First line contains the vertex intervals + dv = 1023.0 / float(lut_size-1) + for i in range(lut_size): + lut_intervals.append(np.uint16(dv * i)) + # Texels are in R G B per line with indices increasing first with blue, then green, and then red. + for r in range(lut_size): + for g in range(lut_size): + for b in range(lut_size): + uv = get_uv_coord(lut_size, r, g, b) + px = np.array(image_buffer.getpixel(uv[0], uv[1]), dtype='f') + px = np.clip(px, 0.0, 1.0) + px = np.uint16(px * 4095) + lut_values.append(px) + + return lut_intervals, lut_values + +# To Do: add some input file validation +# If the input file doesn't exist, you'll get a LUT with res of 0 x 0 and result in a math error +#Resolution is 0 x 0 +#writing C:\Depot\o3de-engine\Gems\AtomLyIntegration\CommonFeatures\Tools\ColorGrading\TestData\Nuke\HDR\Nuke_Post_grade_LUT.3dl... +#Traceback (most recent call last): + #File "..\..\Editor\Scripts\ColorGrading\exr_to_3dl_azasset.py", line 103, in + #dv = 1023.0 / float(lutSize) +# ZeroDivisionError: float division by zero + +def write_3DL(file_path, lut_size, lut_intervals, lut_values): + lut_file_path = f'{file_path}.3dl' + _LOGGER.info(f"Writing {lut_file_path}...") + lut_file = open(lut_file_path, 'w') + for i in range(lut_size): + lut_file.write(f"{lut_intervals[i]} ") + lut_file.write("\n") + for px in lut_values: + lut_file.write(f"{px[0]} {px[1]} {px[2]}\n") + lut_file.close() + diff --git a/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/exr_to_3dl_azasset.py b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/exr_to_3dl_azasset.py index a3aae9cb94..3186b7498f 100644 --- a/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/exr_to_3dl_azasset.py +++ b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/exr_to_3dl_azasset.py @@ -46,48 +46,7 @@ from ColorGrading.from_3dl_to_azasset import write_azasset from ColorGrading import get_uv_coord -def generate_lut_values(image_spec, image_buffer): - lut_size = image_spec.height - - lut_intervals = [] - lut_values = [] - - # First line contains the vertex intervals - dv = 1023.0 / float(lut_size-1) - for i in range(lut_size): - lut_intervals.append(np.uint16(dv * i)) - # Texels are in R G B per line with indices increasing first with blue, then green, and then red. - for r in range(lut_size): - for g in range(lut_size): - for b in range(lut_size): - uv = get_uv_coord(lut_size, r, g, b) - px = np.array(image_buffer.getpixel(uv[0], uv[1]), dtype='f') - px = np.clip(px, 0.0, 1.0) - px = np.uint16(px * 4095) - lut_values.append(px) - - return lut_intervals, lut_values - -# To Do: add some input file validation -# If the input file doesn't exist, you'll get a LUT with res of 0 x 0 and result in a math error -#Resolution is 0 x 0 -#writing C:\Depot\o3de-engine\Gems\AtomLyIntegration\CommonFeatures\Tools\ColorGrading\TestData\Nuke\HDR\Nuke_Post_grade_LUT.3dl... -#Traceback (most recent call last): - #File "..\..\Editor\Scripts\ColorGrading\exr_to_3dl_azasset.py", line 103, in - #dv = 1023.0 / float(lutSize) -# ZeroDivisionError: float division by zero - -def write_3DL(file_path, lut_size, lut_intervals, lut_values): - lut_file_path = f'{file_path}.3dl' - _LOGGER.info(f"Writing {lut_file_path}...") - lut_file = open(lut_file_path, 'w') - for i in range(lut_size): - lut_file.write(f"{lut_intervals[i]} ") - lut_file.write("\n") - for px in lut_values: - lut_file.write(f"{px[0]} {px[1]} {px[2]}\n") - lut_file.close() - +from ColorGrading.azasset_converter_utils import generate_lut_values, write_3DL ########################################################################### # Main Code Block, runs this script as main (testing) diff --git a/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/tiff_to_3dl_azasset.py b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/tiff_to_3dl_azasset.py new file mode 100644 index 0000000000..479aa40976 --- /dev/null +++ b/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/tiff_to_3dl_azasset.py @@ -0,0 +1,81 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +""" + input: a shaped .tiff representing a LUT (for instance coming out of photoshop) + output: a inverse shaped LUT as .tiff + ^ as a .3DL (normalized lut file type) + ^ as a .azasset (for o3de engine) +""" + +import sys +import os +import argparse +import math +import site +import pathlib +from pathlib import Path +import logging as _logging +import numpy as np + +# ------------------------------------------------------------------------ +_MODULENAME = 'ColorGrading.tiff_to_3dl_azasset' + +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) + +import ColorGrading.initialize +if ColorGrading.initialize.start(): + try: + import OpenImageIO as oiio + pass + except ImportError as e: + _LOGGER.error(f"invalid import: {e}") + sys.exit(1) +# ------------------------------------------------------------------------ + + +# ------------------------------------------------------------------------ +from ColorGrading.from_3dl_to_azasset import write_azasset + +from ColorGrading.azasset_converter_utils import generate_lut_values, write_3DL + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + parser=argparse.ArgumentParser() + parser.add_argument('--i', type=str, required=True, help='input file') + parser.add_argument('--o', type=str, required=True, help='output file') + args=parser.parse_args() + + # Read input image + image_buffer=oiio.ImageBuf(args.i) + image_spec=image_buffer.spec() + + #img = oiio.ImageInput.open(args.i) + #_LOGGER.info(f"Resolution is, x: {img.spec().width} and y: {img.spec().height}") + + _LOGGER.info(f"Resolution is, x: {image_buffer.spec().width} and y: {image_buffer.spec().height}") + + if image_spec.width != image_spec.height * image_spec.height: + _LOGGER.info(f"invalid input file dimensions. Expect lengthwise LUT with dimension W: s*s X H: s, where s is the size of the LUT") + sys.exit(1) + + lut_intervals, lut_values = generate_lut_values(image_spec, image_buffer) + + write_3DL(args.o, image_spec.height, lut_intervals, lut_values) + + # write_azasset(file_path, lut_intervals, lut_values, azasset_json=AZASSET_LUT) + write_azasset(args.o, lut_intervals, lut_values) + + # example from command line + # python % DCCSI_COLORGRADING_SCRIPTS %\lut_helper.py - -i C: \Depot\o3de\Gems\Atom\Feature\Common\Tools\ColorGrading\Resources\LUTs\linear_32_LUT.tiff - -op pre - grading - -shaper Log2 - 48nits - -o C: \Depot\o3de\Gems\Atom\Feature\Common\Tools\ColorGrading\Resources\LUTs\base_Log2-48nits_32_LUT.exr diff --git a/Gems/Atom/Feature/Common/Editor/atom_feature_common_editor_script_files.cmake b/Gems/Atom/Feature/Common/Editor/atom_feature_common_editor_script_files.cmake new file mode 100644 index 0000000000..c9fa76e7dc --- /dev/null +++ b/Gems/Atom/Feature/Common/Editor/atom_feature_common_editor_script_files.cmake @@ -0,0 +1,16 @@ +# +# 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 + Scripts/ColorGrading/__init__.py + Scripts/ColorGrading/initialize.py + Scripts/ColorGrading/azasset_converter_utils.py + Scripts/ColorGrading/from_3dl_to_azasset.py + Scripts/ColorGrading/tiff_to_3dl_azasset.py + Scripts/ColorGrading/activate_lut_asset.py +) diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h index b0c99248f1..a47e186e3b 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/FrameSchedulerEnums.h @@ -66,9 +66,6 @@ namespace AZ { None = 0, - //! Enables gathering of cpu timing statistics. - GatherCpuTimingStatistics = AZ_BIT(0), - //! Enables gathering of transient attachment statistics. GatherTransientAttachmentStatistics = AZ_BIT(2), diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h deleted file mode 100644 index 70c5771b57..0000000000 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h +++ /dev/null @@ -1,87 +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 - * - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace AZ -{ - namespace RHI - { - //! Structure that is used to cache a timed region into the thread's local storage. - struct CachedTimeRegion - { - //! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region - //! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown. - struct GroupRegionName - { - GroupRegionName() = delete; - GroupRegionName(const char* const group, const char* const region); - - const char* m_groupName = nullptr; - const char* m_regionName = nullptr; - - struct Hash - { - AZStd::size_t operator()(const GroupRegionName& name) const; - }; - bool operator==(const GroupRegionName& other) const; - }; - - CachedTimeRegion() = default; - CachedTimeRegion(const GroupRegionName& groupRegionName); - CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); - - GroupRegionName m_groupRegionName{nullptr, nullptr}; - - uint16_t m_stackDepth = 0u; - AZStd::sys_time_t m_startTick = 0; - AZStd::sys_time_t m_endTick = 0; - }; - - //! Interface class of the CpuProfiler - class CpuProfiler - { - public: - using ThreadTimeRegionMap = AZStd::unordered_map>; - using TimeRegionMap = AZStd::unordered_map; - - AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); - - CpuProfiler() = default; - virtual ~CpuProfiler() = default; - - AZ_DISABLE_COPY_MOVE(CpuProfiler); - - static CpuProfiler* Get(); - - //! Get the last frame's TimeRegionMap - virtual const TimeRegionMap& GetTimeRegionMap() const = 0; - - //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. - [[nodiscard]] virtual bool BeginContinuousCapture() = 0; - - //! Flush the CPU Profiler's saved data into the passed ring buffer . - [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; - - virtual bool IsContinuousCaptureInProgress() const = 0; - - //! Enable/Disable the CpuProfiler - virtual void SetProfilerEnabled(bool enabled) = 0; - - virtual bool IsProfilerEnabled() const = 0 ; - }; - - } // namespace RPI -} // namespace AZ diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h deleted file mode 100644 index 29886625ea..0000000000 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h +++ /dev/null @@ -1,188 +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 - * - */ -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace AZ -{ - namespace RHI - { - //! Thread local class to keep track of the thread's cached time regions. - //! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl. - //! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage. - class CpuTimingLocalStorage : - public AZStd::intrusive_refcount - { - friend class CpuProfilerImpl; - - public: - AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); - - CpuTimingLocalStorage(); - ~CpuTimingLocalStorage(); - - private: - // Maximum stack size - static constexpr uint32_t TimeRegionStackSize = 2048u; - - // Adds a region to the stack, gets called each time a region begins - void RegionStackPushBack(CachedTimeRegion& timeRegion); - - // Pops a region from the stack, gets called each time a region ends - void RegionStackPopBack(); - - // Add a new cached time region. If the stack is empty, flush all entries to the cached map - void AddCachedRegion(const CachedTimeRegion& timeRegionCached); - - // Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked - void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap); - - AZStd::thread_id m_executingThreadId; - // Keeps track of the current thread's stack depth - uint32_t m_stackLevel = 0u; - - // Cached region map, will be flushed to the system's map when the system requests it - CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap; - - // Use fixed vectors to avoid re-allocating new elements - // Keeps track of the regions that added and removed using the macro - AZStd::fixed_vector m_timeRegionStack; - - // Keeps track of regions that completed (i.e regions that was pushed and popped from the stack) - // Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be - // copied to the map. - AZStd::fixed_vector m_cachedTimeRegions; - AZStd::mutex m_cachedTimeRegionMutex; - - // Dirty flag which is set when the CpuProfiler's enabled state is set from false to true - AZStd::atomic_bool m_clearContainers = false; - - // When the thread is terminated, it will flag itself for deletion - AZStd::atomic_bool m_deleteFlag = false; - - // Keep track of the regions that have hit the size limit so we don't have to lock to check - AZStd::map m_hitSizeLimitMap; - }; - - //! CpuProfiler will keep track of the registered threads, and - //! forwards the request to profile a region to the appropriate thread. The user is able to request all - //! cached regions, which are stored on a per thread frequency. - class CpuProfilerImpl final - : public AZ::Debug::Profiler - , public CpuProfiler - , public SystemTickBus::Handler - { - friend class CpuTimingLocalStorage; - - public: - AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}"); - AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0); - - CpuProfilerImpl() = default; - ~CpuProfilerImpl() = default; - - //! Registers the CpuProfilerImpl instance to the interface - void Init(); - //! Unregisters the CpuProfilerImpl instance from the interface - void Shutdown(); - - // SystemTickBus::Handler overrides - // When fired, the profiler collects all profiling data from registered threads and updates - // m_timeRegionMap so that the next frame has up-to-date profiling data. - void OnSystemTick() final override; - - //! AZ::Debug::Profiler overrides... - void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override; - void EndRegion(const AZ::Debug::Budget* budget) final override; - - //! CpuProfiler overrides... - const TimeRegionMap& GetTimeRegionMap() const final override; - bool BeginContinuousCapture() final override; - bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) final override; - bool IsContinuousCaptureInProgress() const final override; - void SetProfilerEnabled(bool enabled) final override; - bool IsProfilerEnabled() const final override; - - private: - static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps - static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings. - - // Lazily create and register the local thread data - void RegisterThreadStorage(); - - // ThreadId -> ThreadTimeRegionMap - // On the start of each frame, this map will be updated with the last frame's profiling data. - TimeRegionMap m_timeRegionMap; - - // Set of registered threads when created - AZStd::vector, AZ::OSStdAllocator> m_registeredThreads; - AZStd::mutex m_threadRegisterMutex; - - // Thread local storage, gets lazily allocated when a thread is created - static thread_local CpuTimingLocalStorage* ms_threadLocalStorage; - - // Enable/Disables the threads from profiling - AZStd::atomic_bool m_enabled = false; - - // This lock will only be contested when the CpuProfiler's Shutdown() method has been called - AZStd::shared_mutex m_shutdownMutex; - - bool m_initialized = false; - - AZStd::mutex m_continuousCaptureEndingMutex; - - AZStd::atomic_bool m_continuousCaptureInProgress; - - // Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called. - // Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality. - AZStd::ring_buffer m_continuousCaptureData; - }; - - // Intermediate class to serialize Cpu TimedRegion data. - class CpuProfilingStatisticsSerializer - { - public: - class CpuProfilingStatisticsSerializerEntry - { - public: - AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}"); - static void Reflect(AZ::ReflectContext* context); - - CpuProfilingStatisticsSerializerEntry() = default; - CpuProfilingStatisticsSerializerEntry(const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId); - - Name m_groupName; - Name m_regionName; - uint16_t m_stackDepth; - AZStd::sys_time_t m_startTick; - AZStd::sys_time_t m_endTick; - size_t m_threadId; - }; - - AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}"); - static void Reflect(AZ::ReflectContext* context); - - CpuProfilingStatisticsSerializer() = default; - CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData); - - AZStd::vector m_cpuProfilingStatisticsSerializerEntries; - }; - }; // namespace RHI -}; // namespace AZ diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h index 52a44c0903..6d416b77fc 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/RHISystem.h @@ -8,7 +8,6 @@ #pragma once -#include #include #include #include @@ -66,8 +65,6 @@ namespace AZ RHI::Ptr m_pipelineStateCache; RHI::FrameScheduler m_frameScheduler; RHI::FrameSchedulerCompileRequest m_compileRequest; - - RHI::CpuProfilerImpl m_cpuProfiler; }; } // namespace RPI } // namespace AZ diff --git a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp deleted file mode 100644 index 826e0b6aa3..0000000000 --- a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp +++ /dev/null @@ -1,448 +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 - -#include -#include - -#include -#include -#include - -namespace AZ -{ - namespace RHI - { - thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; - - // --- CpuProfiler --- - - CpuProfiler* CpuProfiler::Get() - { - return Interface::Get(); - } - - // --- CachedTimeRegion --- - - CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName) - { - m_groupRegionName = groupRegionName; - } - - CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) - { - m_groupRegionName = groupRegionName; - m_stackDepth = stackDepth; - m_startTick = startTick; - m_endTick = endTick; - } - - // --- GroupRegionName --- - - CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) : - m_groupName(group), - m_regionName(region) - { - } - - AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const - { - AZStd::size_t seed = 0; - AZStd::hash_combine(seed, name.m_groupName); - AZStd::hash_combine(seed, name.m_regionName); - return seed; - } - - bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const - { - return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); - } - - - // --- CpuProfilerImpl --- - - void CpuProfilerImpl::Init() - { - Interface::Register(this); - Interface::Register(this); - m_initialized = true; - SystemTickBus::Handler::BusConnect(); - m_continuousCaptureData.set_capacity(10); - - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - statsProfiler->ActivateProfiler(AZ_CRC_CE("RHI"), true); - } - } - - void CpuProfilerImpl::Shutdown() - { - if (!m_initialized) - { - return; - } - // When this call is made, no more thread profiling calls can be performed anymore - Interface::Unregister(this); - Interface::Unregister(this); - - // Wait for the remaining threads that might still be processing its profiling calls - AZStd::unique_lock shutdownLock(m_shutdownMutex); - - m_enabled = false; - - // Cleanup all TLS - m_registeredThreads.clear(); - m_timeRegionMap.clear(); - m_initialized = false; - m_continuousCaptureInProgress.store(false); - m_continuousCaptureData.clear(); - SystemTickBus::Handler::BusDisconnect(); - } - - void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) - { - // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. - if (m_shutdownMutex.try_lock_shared()) - { - if (m_enabled) - { - // Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it - RegisterThreadStorage(); - - // Push it to the stack - CachedTimeRegion timeRegion({budget->Name(), eventName}); - ms_threadLocalStorage->RegionStackPushBack(timeRegion); - } - - m_shutdownMutex.unlock_shared(); - } - } - - void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget) - { - // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. - if (m_shutdownMutex.try_lock_shared()) - { - // guard against enabling mid-marker - if (m_enabled && ms_threadLocalStorage != nullptr) - { - ms_threadLocalStorage->RegionStackPopBack(); - } - - m_shutdownMutex.unlock_shared(); - } - } - - const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const - { - return m_timeRegionMap; - } - - bool CpuProfilerImpl::BeginContinuousCapture() - { - bool expected = false; - if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true)) - { - m_enabled = true; - AZ_TracePrintf("Profiler", "Continuous capture started\n"); - return true; - } - - AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress"); - return false; - } - - bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) - { - if (!m_continuousCaptureInProgress.load()) - { - AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress"); - return false; - } - - if (m_continuousCaptureEndingMutex.try_lock()) - { - m_enabled = false; - flushTarget = AZStd::move(m_continuousCaptureData); - m_continuousCaptureData.clear(); - AZ_TracePrintf("Profiler", "Continuous capture ended\n"); - m_continuousCaptureInProgress.store(false); - - m_continuousCaptureEndingMutex.unlock(); - return true; - } - - return false; - } - - bool CpuProfilerImpl::IsContinuousCaptureInProgress() const - { - return m_continuousCaptureInProgress.load(); - } - - void CpuProfilerImpl::SetProfilerEnabled(bool enabled) - { - AZStd::unique_lock lock(m_threadRegisterMutex); - - // Early out if the state is already the same or a continuous capture is in progress - if (m_enabled == enabled || m_continuousCaptureInProgress.load()) - { - return; - } - - // Set the dirty flag in all the TLS to clear the caches - if (enabled) - { - // Iterate through all the threads, and set the clearing flag - for (auto& threadLocal : m_registeredThreads) - { - threadLocal->m_clearContainers = true; - } - - m_enabled = true; - } - else - { - m_enabled = false; - } - } - - bool CpuProfilerImpl::IsProfilerEnabled() const - { - return m_enabled; - } - - void CpuProfilerImpl::OnSystemTick() - { - if (!m_enabled) - { - return; - } - - if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) - { - if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave) - { - const AZStd::size_t size = m_continuousCaptureData.size(); - m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2)); - } - - m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap)); - m_timeRegionMap.clear(); - m_continuousCaptureEndingMutex.unlock(); - } - - AZStd::unique_lock lock(m_threadRegisterMutex); - - // Iterate through all the threads, and collect the thread's cached time regions - TimeRegionMap newMap; - for (auto& threadLocal : m_registeredThreads) - { - ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId]; - threadLocal->TryFlushCachedMap(threadMapEntry); - } - - // Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated - AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const RHI::Ptr& thread) - { - return thread->m_deleteFlag.load(); - }); - - // Update our saved time regions to the last frame's collected data - m_timeRegionMap = AZStd::move(newMap); - } - - void CpuProfilerImpl::RegisterThreadStorage() - { - AZStd::unique_lock lock(m_threadRegisterMutex); - if (!ms_threadLocalStorage) - { - ms_threadLocalStorage = aznew CpuTimingLocalStorage(); - m_registeredThreads.emplace_back(ms_threadLocalStorage); - } - } - - // --- CpuTimingLocalStorage --- - - CpuTimingLocalStorage::CpuTimingLocalStorage() - { - m_executingThreadId = AZStd::this_thread::get_id(); - } - - CpuTimingLocalStorage::~CpuTimingLocalStorage() - { - m_deleteFlag = true; - } - - void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) - { - // If it was (re)enabled, clear the lists first - if (m_clearContainers) - { - m_clearContainers = false; - - m_stackLevel = 0; - m_cachedTimeRegionMap.clear(); - m_timeRegionStack.clear(); - m_cachedTimeRegions.clear(); - } - - timeRegion.m_stackDepth = static_cast(m_stackLevel); - - AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize."); - m_timeRegionStack.push_back(timeRegion); - - // Increment the stack - m_stackLevel++; - - // Set the starting time at the end, to avoid recording the minor overhead - m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks(); - } - - void CpuTimingLocalStorage::RegionStackPopBack() - { - // Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers - if (m_timeRegionStack.empty()) - { - return; - } - - // Get the end timestamp here, to avoid the minor overhead - const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks(); - - AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty."); - CachedTimeRegion back = m_timeRegionStack.back(); - m_timeRegionStack.pop_back(); - - // Set the ending time - back.m_endTick = endRegionTime; - - // Decrement the stack - m_stackLevel--; - - // Add an entry to the cached region - AddCachedRegion(back); - } - - // Gets called when region ends and all data is set - void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached) - { - if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName]) - { - return; - } - // Add an entry to the cached region - m_cachedTimeRegions.push_back(timeRegionCached); - - // If the stack is empty, add it to the local cache map. Only gets called when the stack is empty - // NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty - // (i.e when the root region ended), this overhead won't affect any time regions. - // The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this - // case, the overhead of the profiled threads will be added to the main thread. - if (m_timeRegionStack.empty()) - { - AZStd::unique_lock lock(m_cachedTimeRegionMutex); - - // Add the cached regions to the map - for (auto& cachedTimeRegion : m_cachedTimeRegions) - { - const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName; - AZStd::vector& regionVec = m_cachedTimeRegionMap[regionName]; - regionVec.push_back(cachedTimeRegion); - if (regionVec.size() >= TimeRegionStackSize) - { - m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); - } - } - - // Clear the cached regions - m_cachedTimeRegions.clear(); - } - } - - void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap) - { - // Try to lock, if it's already in use (the cached regions in the array are being copied to the map) - // it'll show up in the next iteration when the user requests it. - if (m_cachedTimeRegionMutex.try_lock()) - { - // Only flush cached time regions if there are entries available - if (!m_cachedTimeRegionMap.empty()) - { - cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap); - m_cachedTimeRegionMap.clear(); - m_hitSizeLimitMap.clear(); - } - m_cachedTimeRegionMutex.unlock(); - } - } - - // --- CpuProfilingStatisticsSerializer --- - - CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) - { - // Create serializable entries - for (const auto& timeRegionMap : continuousData) - { - for (const auto& [threadId, regionMap] : timeRegionMap) - { - for (const auto& [regionName, regionVec] : regionMap) - { - for (const auto& region : regionVec) - { - m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); - } - } - } - } - } - - void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) - { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries) - ; - } - - CpuProfilingStatisticsSerializerEntry::Reflect(context); - } - - // --- CpuProfilingStatisticsSerializerEntry --- - - CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry( - const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId) - { - m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName; - m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName; - m_stackDepth = cachedTimeRegion.m_stackDepth; - m_startTick = cachedTimeRegion.m_startTick; - m_endTick = cachedTimeRegion.m_endTick; - m_threadId = AZStd::hash{}(threadId); - } - - void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context) - { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName) - ->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName) - ->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth) - ->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick) - ->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick) - ->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId) - ; - } - } - } // namespace RHI -} // namespace AZ diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp index a15db9e24b..11ae78c69a 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp @@ -86,6 +86,8 @@ namespace AZ if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) { + statsProfiler->ActivateProfiler(rhiMetricsId, true); + auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); rhiMetrics.GetStatsManager().AddStatistic(frameTimeMetricId, frameTimeMetricName, /*units=*/"clocks", /*failIfExist=*/false); } @@ -602,14 +604,11 @@ namespace AZ double FrameScheduler::GetCpuFrameTime() const { - if (CheckBitsAny(m_compileRequest.m_statisticsFlags, FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics)) + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) { - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); - const auto* frameTimeStat = rhiMetrics.GetStatistic(frameTimeMetricId); - return (frameTimeStat->GetMostRecentSample() * 1000) / aznumeric_cast(AZStd::GetTimeTicksPerSecond()); - } + auto& rhiMetrics = statsProfiler->GetProfiler(rhiMetricsId); + const auto* frameTimeStat = rhiMetrics.GetStatistic(frameTimeMetricId); + return (frameTimeStat->GetMostRecentSample() * 1000) / aznumeric_cast(AZStd::GetTimeTicksPerSecond()); } return 0; } diff --git a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp index b40ad3e11a..e7515bbf6e 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp @@ -39,8 +39,6 @@ namespace AZ void RHISystem::Init() { - m_cpuProfiler.Init(); - Ptr platformLimitsDescriptor = m_device->GetDescriptor().m_platformLimitsDescriptor; RHI::FrameSchedulerDescriptor frameSchedulerDescriptor; @@ -187,8 +185,6 @@ namespace AZ AZ_Assert(m_device->use_count()==1, "The ref count for Device is %i but it should be 1 here to ensure all the resources are released", m_device->use_count()); m_device = nullptr; } - - m_cpuProfiler.Shutdown(); } void RHISystem::FrameUpdate(FrameGraphCallback frameGraphCallback) diff --git a/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake b/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake index 338bae6386..60ae24ae9b 100644 --- a/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake +++ b/Gems/Atom/RHI/Code/atom_rhi_public_files.cmake @@ -197,8 +197,5 @@ set(FILES Include/Atom/RHI/interval_map.h Include/Atom/RHI/ImageProperty.h Include/Atom/RHI/BufferProperty.h - Include/Atom/RHI/CpuProfiler.h - Include/Atom/RHI/CpuProfilerImpl.h - Source/RHI/CpuProfilerImpl.cpp Include/Atom/RHI/TagRegistry.h ) diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h index 268285adc0..8233658ded 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Viewport/RenderViewportWidget.h @@ -102,6 +102,8 @@ namespace AtomToolsFramework void BeginCursorCapture() override; void EndCursorCapture() override; bool IsMouseOver() const override; + void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override; + void ClearOverrideCursor() override; // AzFramework::WindowRequestBus::Handler overrides ... void SetWindowTitle(const AZStd::string& title) override; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRendererSystemComponent.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRendererSystemComponent.cpp index f88adfc65d..fc5b31297a 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRendererSystemComponent.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRendererSystemComponent.cpp @@ -63,8 +63,11 @@ namespace AtomToolsFramework void PreviewRendererSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile) { AZ::TickBus::QueueFunction([this](){ - m_previewRenderer.reset(aznew AtomToolsFramework::PreviewRenderer( - "PreviewRendererSystemComponent Preview Scene", "PreviewRendererSystemComponent Preview Pipeline")); + if (!m_previewRenderer) + { + m_previewRenderer.reset(aznew AtomToolsFramework::PreviewRenderer( + "PreviewRendererSystemComponent Preview Scene", "PreviewRendererSystemComponent Preview Pipeline")); + } }); } diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index e8975c0b62..e43880827c 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -375,6 +375,16 @@ namespace AtomToolsFramework m_inputChannelMapper->SetCursorCaptureEnabled(false); } + void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) + { + m_inputChannelMapper->SetOverrideCursor(cursorStyleOverride); + } + + void RenderViewportWidget::ClearOverrideCursor() + { + m_inputChannelMapper->ClearOverrideCursor(); + } + void RenderViewportWidget::SetWindowTitle(const AZStd::string& title) { setWindowTitle(QString::fromUtf8(title.c_str())); diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp index c3bb13d1d2..563b2754df 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/PerformanceMonitorComponent.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -66,12 +65,6 @@ namespace MaterialEditor AZ_Error("PerformanceMonitorComponent", false, "Failed to find root pass."); } - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, - enabled); - - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(enabled); - if (enabled) { ResetStats(); diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h deleted file mode 100644 index 4a326660dd..0000000000 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ /dev/null @@ -1,231 +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 - * - */ - -#pragma once - -#include -#include -#include - -#include - - -namespace AZ -{ - namespace Render - { - //! Stores all the data associated with a row in the table. - struct TableRow - { - template - struct TableRowCompareFunctor - { - TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; - - bool operator()(const TableRow* lhs, const TableRow* rhs) - { - return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer; - } - - T m_memberPointer; - bool m_ascending; - }; - - // Update running statistics with new region data - void RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId); - - void ResetPerFrameStatistics(); - - // Get a string of all threads that this region executed in during the last frame - AZStd::string GetExecutingThreadsLabel() const; - - AZStd::string m_groupName; - AZStd::string m_regionName; - - // --- Per frame statistics --- - - u64 m_invocationsLastFrame = 0; - - // NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip. - AZStd::set m_executingThreads; - - AZStd::sys_time_t m_lastFrameTotalTicks = 0; - - // Maximum execution time of a region in the last frame. - AZStd::sys_time_t m_maxTicks = 0; - - // --- Aggregate statistics --- - - u64 m_invocationsTotal = 0; - - // Running average of Mean Time Per Call - AZStd::sys_time_t m_runningAverageTicks = 0; - }; - - //! ImGui widget for examining Atom CPU Profiling instrumentation. - //! Offers both a statistical view (with sorting and searching capability) and a visualizer - //! similar to RAD and other profiling tools. - class ImGuiCpuProfiler - : SystemTickBus::Handler - { - // Region Name -> statistical view row data - using RegionRowMap = AZStd::map; - // Group Name -> RegionRowMap - using GroupRegionMap = AZStd::map; - - using TimeRegion = AZ::RHI::CachedTimeRegion; - using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName; - - public: - struct CpuTimingEntry - { - const AZStd::string& m_name; - double m_executeDuration; - }; - - ImGuiCpuProfiler() = default; - ~ImGuiCpuProfiler() = default; - - //! Draws the overall CPU profiling window, defaults to the statistical view - void Draw(bool& keepDrawing); - - private: - static constexpr float RowHeight = 35.0; - static constexpr int DefaultFramesToCollect = 50; - static constexpr float MediumFrameTimeLimit = 16.6; // 60 fps - static constexpr float HighFrameTimeLimit = 33.3; // 30 fps - - //! Draws the statistical view of the CPU profiling data. - void DrawStatisticsView(); - - //! Callback invoked when the "Load File" button is pressed in the file picker. - void LoadFile(); - - //! Draws the file picker window. - void DrawFilePicker(); - - //! Draws the CPU profiling visualizer. - void DrawVisualizer(); - - // Draw the shared header between the two windows. - void DrawCommonHeader(); - - // Draw the region statistics table in the order specified by the pointers in m_tableData. - void DrawTable(); - - // Sort the table by a given column, rearranges the pointers in m_tableData. - void SortTable(ImGuiTableSortSpecs* sortSpecs); - - // gather the latest timing statistics - void CacheCpuTimingStatistics(); - - // Get the profiling data from the last frame, only called when the profiler is not paused. - void CollectFrameData(); - - // Cull old data from internal storage, only called when profiler is not paused. - void CullFrameData(); - - // Draws a single block onto the timeline into the specified row - void DrawBlock(const TimeRegion& block, u64 targetRow); - - // Draw horizontal lines between threads in the timeline - void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth); - - // Draw the "Thread XXXXX" label onto the viewport - void DrawThreadLabel(u64 baseRow, size_t threadId); - - // Draw the vertical lines separating frames in the timeline - void DrawFrameBoundaries(); - - // Draw the ruler with frame time labels - void DrawRuler(); - - // Draw the frame time histogram - void DrawFrameTimeHistogram(); - - // Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling - float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const; - - AZStd::sys_time_t GetViewportTickWidth() const; - - // Gets the color for a block using the GroupRegionName as a key into the cache. - // Generates a random ImU32 if the block does not yet have a color. - ImU32 GetBlockColor(const TimeRegion& block); - - // System tick bus overrides - virtual void OnSystemTick() override; - - // --- Visualizer Members --- - - int m_framesToCollect = DefaultFramesToCollect; - - // Tally of the number of saved profiling events so far - u64 m_savedRegionCount = 0; - - // Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects - AZStd::sys_time_t m_viewportStartTick; - AZStd::sys_time_t m_viewportEndTick; - - // Map to store each thread's TimeRegions, individual vectors are sorted by start tick - // note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from - // platform to platform, which causes problems when deserializing saved captures. - AZStd::unordered_map> m_savedData; - - // Region color cache - AZStd::unordered_map m_regionColorMap; - - // Tracks the frame boundaries - AZStd::vector m_frameEndTicks = { INT64_MIN }; - - // Filter for highlighting regions on the visualizer - ImGuiTextFilter m_visualizerHighlightFilter; - - // --- Tabular view members --- - - // ImGui filter used to filter TimedRegions. - ImGuiTextFilter m_timedRegionFilter; - - // Saves statistical view data organized by group name -> region name -> row data - GroupRegionMap m_groupRegionMap; - - // Saves pointers to objects in m_groupRegionMap, order reflects table ordering. - // Non-owning, will be cleared when m_groupRegionMap is cleared. - AZStd::vector m_tableData; - - // Pause cpu profiling. The profiler will show the statistics of the last frame before pause. - bool m_paused = false; - - // Export the profiling data from a single frame to a local file. - bool m_captureToFile = false; - - // Toggle between the normal statistical view and the visual profiling view. - bool m_enableVisualizer = false; - - // Last captured CPU timing statistics - AZStd::vector m_cpuTimingStatisticsWhenPause; - AZStd::sys_time_t m_frameToFrameTime{}; - - AZStd::string m_lastCapturedFilePath; - - bool m_showFilePicker = false; - - // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. - AZStd::vector m_cachedCapturePaths; - - // Index into the file picker, used to determine which file to load when "Load File" is pressed. - int m_currentFileIndex = 0; - - - // --- Loading capture state --- - AZStd::unordered_set m_deserializedStringPool; - AZStd::unordered_set m_deserializedGroupRegionNamePool; - }; - } // namespace Render -} // namespace AZ - -#include "ImGuiCpuProfiler.inl" diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl deleted file mode 100644 index daf6eca5c0..0000000000 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ /dev/null @@ -1,1159 +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 -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace AZ -{ - namespace Render - { - namespace CpuProfilerImGuiHelper - { - inline float TicksToMs(double ticks) - { - // Note: converting to microseconds integer before converting to milliseconds float - const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); - AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000"); - return static_cast((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f; - } - - inline float TicksToMs(AZStd::sys_time_t ticks) - { - return TicksToMs(static_cast(ticks)); - } - - using DeserializedCpuData = AZStd::vector; - inline Outcome LoadSavedCpuProfilingStatistics(const AZStd::string& capturePath) - { - auto* base = IO::FileIOBase::GetInstance(); - - char resolvedPath[IO::MaxPathLength]; - if (!base->ResolvePath(capturePath.c_str(), resolvedPath, IO::MaxPathLength)) - { - return Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath)); - } - - u64 captureSizeBytes; - const IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes); - if (!fileSizeResult) - { - return Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath)); - } - - // NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because - // saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory. - FILE* fp = nullptr; - azfopen(&fp, resolvedPath, "rb"); - if (!fp) - { - return Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath)); - } - - constexpr AZStd::size_t MaxBufSize = 65536; - const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); - char* buf = reinterpret_cast(azmalloc(bufSize)); - - rapidjson::Document document; - rapidjson::FileReadStream inputStream(fp, buf, bufSize); - document.ParseStream(inputStream); - - azfree(buf); - fclose(fp); - - if (document.HasParseError()) - { - const auto pe = document.GetParseError(); - return Failure(AZStd::string::format( - "Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe)); - } - - if (!document.IsObject() || !document.HasMember("ClassData")) - { - return Failure(AZStd::string::format( - "Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n")); - } - - AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n"); - - const auto& root = document["ClassData"]; - RHI::CpuProfilingStatisticsSerializer serializer; - const JsonSerializationResult::ResultCode deserializationResult = JsonSerialization::Load(serializer, root); - if (deserializationResult.GetProcessing() == JsonSerializationResult::Processing::Halted - || serializer.m_cpuProfilingStatisticsSerializerEntries.empty()) - { - return Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath.c_str()).c_str())); - } - - AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n", - serializer.m_cpuProfilingStatisticsSerializerEntries.size()); - - return Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); - } - } // namespace CpuProfilerImGuiHelper - - - - inline void ImGuiCpuProfiler::Draw(bool& keepDrawing) - { - // Cache the value to detect if it was changed by ImGui(user pressed 'x') - const bool cachedShowCpuProfiler = keepDrawing; - - const ImVec2 windowSize(900.0f, 600.0f); - ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once); - if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None)) - { - // Collect the last frame's profiling data - if (!m_paused) - { - // Update region map and cache the input cpu timing statistics when the profiling is not paused - CacheCpuTimingStatistics(); - - CollectFrameData(); - CullFrameData(); - - // Only listen to system ticks when the profiler is active - if (!SystemTickBus::Handler::BusIsConnected()) - { - SystemTickBus::Handler::BusConnect(); - } - } - - if (m_enableVisualizer) - { - DrawVisualizer(); - } - else - { - DrawStatisticsView(); - } - - if (m_showFilePicker) - { - DrawFilePicker(); - } - } - ImGui::End(); - - if (m_captureToFile) - { - AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond(); - AZStd::string timeString; - AZStd::to_string(timeString, timeNow); - u64 currentTick = AZ::RPI::RPISystemInterface::Get()->GetCurrentTick(); - const AZStd::string frameDataFilePath = AZStd::string::format( - "@user@/CpuProfiler/%s_%llu.json", - timeString.c_str(), - currentTick); - char resolvedPath[AZ::IO::MaxPathLength]; - AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); - m_lastCapturedFilePath = resolvedPath; - AZ::Render::ProfilingCaptureRequestBus::Broadcast( - &AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath); - } - m_captureToFile = false; - - // Toggle if the bool isn't the same as the cached value - if (cachedShowCpuProfiler != keepDrawing) - { - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); - } - } - - inline void ImGuiCpuProfiler::DrawCommonHeader() - { - if (!m_lastCapturedFilePath.empty()) - { - ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); - } - - if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) - { - m_enableVisualizer = !m_enableVisualizer; - } - - ImGui::SameLine(); - m_paused = !AZ::RHI::CpuProfiler::Get()->IsProfilerEnabled(); - if (ImGui::Button(m_paused ? "Resume" : "Pause")) - { - m_paused = !m_paused; - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(!m_paused); - } - - ImGui::SameLine(); - if (ImGui::Button("Capture")) - { - m_captureToFile = true; - } - - ImGui::SameLine(); - bool isInProgress = RHI::CpuProfiler::Get()->IsContinuousCaptureInProgress(); - if (ImGui::Button(isInProgress ? "End" : "Begin")) - { - if (isInProgress) - { - AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond(); - AZStd::string timeString; - AZStd::to_string(timeString, timeNow); - u64 currentTick = AZ::RPI::RPISystemInterface::Get()->GetCurrentTick(); - const AZStd::string frameDataFilePath = AZStd::string::format( - "@user@/CpuProfiler/%s_%llu.json", - timeString.c_str(), - currentTick); - char resolvedPath[AZ::IO::MaxPathLength]; - AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); - m_lastCapturedFilePath = resolvedPath; - AZ::Render::ProfilingCaptureRequestBus::Broadcast( - &AZ::Render::ProfilingCaptureRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath); - m_paused = true; - } - - else - { - AZ::Render::ProfilingCaptureRequestBus::Broadcast( - &AZ::Render::ProfilingCaptureRequestBus::Events::BeginContinuousCpuProfilingCapture); - } - } - - ImGui::SameLine(); - if (ImGui::Button("Load file")) - { - m_showFilePicker = true; - - // Only update the cached file list when opened so that we aren't making IO calls on every frame. - auto* base = AZ::IO::FileIOBase::GetInstance(); - const AZStd::string defaultSavedCapturePath = "@user@/CpuProfiler"; - - m_cachedCapturePaths.clear(); - base->FindFiles( - defaultSavedCapturePath.c_str(), "*.json", - [&paths = m_cachedCapturePaths](const char* path) -> bool - { - auto foundPath = IO::Path(path); - paths.push_back(foundPath); - return true; - }); - - // Sort by decreasing modification time (most recent at the top) - AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(), - [&base](const IO::Path& lhs, const IO::Path& rhs) - { - return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str()); - }); - } - } - - inline void ImGuiCpuProfiler::DrawTable() - { - const auto flags = - ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable; - if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags)) - { - // Table header setup - ImGui::TableSetupColumn("Group"); - ImGui::TableSetupColumn("Region"); - ImGui::TableSetupColumn("MTPC (ms)"); - ImGui::TableSetupColumn("Max (ms)"); - ImGui::TableSetupColumn("Invocations"); - ImGui::TableSetupColumn("Total (ms)"); - ImGui::TableHeadersRow(); - ImGui::TableNextColumn(); - - ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); - if (sortSpecs && sortSpecs->SpecsDirty) - { - SortTable(sortSpecs); - } - - // Draw all of the rows held in the GroupRegionMap - for (const auto* statistics : m_tableData) - { - if (!m_timedRegionFilter.PassFilter(statistics->m_groupName.c_str()) - && !m_timedRegionFilter.PassFilter(statistics->m_regionName.c_str())) - { - continue; - } - - ImGui::Text("%s", statistics->m_groupName.c_str()); - const ImVec2 topLeftBound = ImGui::GetItemRectMin(); - ImGui::TableNextColumn(); - - ImGui::Text("%s", statistics->m_regionName.c_str()); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); - ImGui::TableNextColumn(); - - ImGui::Text("%llu", statistics->m_invocationsLastFrame); - ImGui::TableNextColumn(); - - ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); - const ImVec2 botRightBound = ImGui::GetItemRectMax(); - ImGui::TableNextColumn(); - - // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because - // ImGui reports incorrect bounds when using Begin/End group in the Tables API. - if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) - { - ImGui::BeginTooltip(); - ImGui::Text("%s", statistics->GetExecutingThreadsLabel().c_str()); - ImGui::EndTooltip(); - } - } - } - ImGui::EndTable(); - } - - inline void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) - { - const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; - const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; - - switch (columnToSort) - { - case (0): // Sort by group name - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_groupName, ascending)); - break; - case (1): // Sort by region name - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_regionName, ascending)); - break; - case (2): // Sort by average time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_runningAverageTicks, ascending)); - break; - case (3): // Sort by max time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_maxTicks, ascending)); - break; - case (4): // Sort by invocations - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending)); - break; - case (5): // Sort by total time - AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending)); - break; - } - sortSpecs->SpecsDirty = false; - } - - inline void ImGuiCpuProfiler::DrawStatisticsView() - { - DrawCommonHeader(); - - const auto ShowRow = [](const char* regionLabel, double duration) - { - ImGui::Text("%s", regionLabel); - ImGui::NextColumn(); - - ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); - ImGui::NextColumn(); - }; - - if (ImGui::BeginChild("Statistics View", { 0, 0 }, true)) - { - // Set column settings. - ImGui::Columns(2, "view", false); - ImGui::SetColumnWidth(0, 660.0f); - ImGui::SetColumnWidth(1, 100.0f); - - for (const auto& queueStatistics : m_cpuTimingStatisticsWhenPause) - { - ShowRow(queueStatistics.m_name.c_str(), queueStatistics.m_executeDuration); - } - - ImGui::Separator(); - ImGui::Columns(1, "view", false); - - m_timedRegionFilter.Draw("Filter"); - ImGui::SameLine(); - if (ImGui::Button("Clear Filter")) - { - m_timedRegionFilter.Clear(); - } - ImGui::SameLine(); - if (ImGui::Button("Reset Table")) - { - m_tableData.clear(); - m_groupRegionMap.clear(); - } - - DrawTable(); - } - } - - inline void ImGuiCpuProfiler::DrawFilePicker() - { - ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); - if (ImGui::Begin("File Picker", &m_showFilePicker)) - { - if (ImGui::Button("Load selected")) - { - LoadFile(); - } - - auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool - { - const auto& pathVec = *static_cast*>(vectorPointer); - if (idx < 0 || idx >= pathVec.size()) - { - return false; - } - *out_text = pathVec[idx].c_str(); - return true; - }; - - ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); - ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); - } - ImGui::End(); - } - - inline void ImGuiCpuProfiler::LoadFile() - { - const IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; - auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.String()); - if (!loadResult.IsSuccess()) - { - AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); - return; - } - - AZStd::vector deserializedData = loadResult.TakeValue(); - - // Clear visualizer and statistics view state - m_savedRegionCount = deserializedData.size(); - m_savedData.clear(); - m_paused = true; - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(false); - m_frameEndTicks.clear(); - - m_tableData.clear(); - m_groupRegionMap.clear(); - - for (const auto& entry : deserializedData) - { - const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetCStr()); - const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetCStr()); - const auto [groupRegionNameItr, wasGroupRegionNameInserted] = - m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str()); - - const RHI::CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick); - m_savedData[entry.m_threadId].push_back(newRegion); - - // Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic. - const static Name frameBoundaryName = Name("RPISystem: OnSystemTick"); - if (entry.m_regionName == frameBoundaryName) - { - m_frameEndTicks.push_back(entry.m_endTick); - } - - // Update running statistics - if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr)) - { - m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr; - m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr; - m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]); - } - m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId); - } - - // Update viewport bounds with some added UX fudge factor - m_viewportStartTick = deserializedData.back().m_startTick - 1000; - m_viewportEndTick = deserializedData.back().m_endTick + 1000; - - // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data. - for (auto& [threadId, singleThreadData] : m_savedData) - { - AZStd::sort(singleThreadData.begin(), singleThreadData.end(), - [](const TimeRegion& lhs, const TimeRegion& rhs) - { - return lhs.m_startTick < rhs.m_startTick; - }); - } - } - - // -- CPU Visualizer -- - inline void ImGuiCpuProfiler::DrawVisualizer() - { - DrawCommonHeader(); - - // Options & Statistics - if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true)) - { - ImGui::Columns(3, "Options", true); - ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 20000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); - m_visualizerHighlightFilter.Draw("Find Region"); - - ImGui::NextColumn(); - - ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth())); - ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick); - ImGui::Text("Recording %zu threads", m_savedData.size()); - ImGui::Text("%llu profiling events saved", m_savedRegionCount); - - ImGui::NextColumn(); - - ImGui::TextWrapped( - "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); - } - - ImGui::Columns(1, "FrameTimeColumn", true); - - if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) - { - DrawFrameTimeHistogram(); - } - ImGui::EndChild(); - - ImGui::Columns(1, "RulerColumn", true); - - // Ruler - if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) - { - DrawRuler(); - } - ImGui::EndChild(); - - - ImGui::Columns(1, "TimelineColumn", true); - - // Timeline - if (ImGui::BeginChild( - "Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) - { - // Find the next frame boundary after the viewport's right bound and draw until that tick - auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick); - if (nextFrameBoundaryItr == m_frameEndTicks.end() && m_frameEndTicks.size() != 0) - { - --nextFrameBoundaryItr; - } - const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; - - // Find the start tick of the leftmost frame, which may be offscreen. - auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); - if (startTickItr != m_frameEndTicks.begin()) - { - --startTickItr; - } - - // Main draw loop - u64 baseRow = 0; - for (const auto& [currentThreadId, singleThreadData] : m_savedData) - { - // Find the first TimeRegion that we should draw - auto regionItr = AZStd::lower_bound( - singleThreadData.begin(), singleThreadData.end(), *startTickItr, - [](const TimeRegion& wrapper, AZStd::sys_time_t target) - { - return wrapper.m_startTick < target; - }); - - if (regionItr == singleThreadData.end()) - { - continue; - } - - // Draw all of the blocks for a given thread/row - u64 maxDepth = 0; - while (regionItr != singleThreadData.end()) - { - const TimeRegion& region = *regionItr; - - // Early out if we have drawn all the onscreen regions - if (region.m_startTick > nextFrameBoundary) - { - break; - } - u64 targetRow = region.m_stackDepth + baseRow; - maxDepth = AZStd::max(aznumeric_cast(region.m_stackDepth), maxDepth); - - DrawBlock(region, targetRow); - - ++regionItr; - } - - // Draw UI details - DrawThreadLabel(baseRow, currentThreadId); - DrawThreadSeparator(baseRow, maxDepth); - - baseRow += maxDepth + 1; // Next draw loop should start one row down - } - - DrawFrameBoundaries(); - - // Draw an invisible button to capture inputs - ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight }); - - // Controls - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsWindowFocused() && ImGui::IsItemHovered()) - { - io.WantCaptureMouse = true; - if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling - { - const auto [deltaX, deltaY] = io.MouseDelta; - if (deltaX != 0 || deltaY != 0) - { - // We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot - // relative to the objects on screen) - const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth(); - auto tickDelta = aznumeric_cast(-1 * pixelDeltaNormalized * GetViewportTickWidth()); - m_viewportStartTick += tickDelta; - m_viewportEndTick += tickDelta; - - ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1); - } - } - else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming - { - // We want zooming to be relative to the mouse's current position - const float mouseX = ImGui::GetMousePos().x; - - // Find the normalized position of the cursor relative to the window - const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth(); - - const auto overallTickDelta = aznumeric_cast(0.05 * io.MouseWheel * GetViewportTickWidth()); - - // Split the overall delta between the two bounds depending on mouse pos - const auto newStartTick = m_viewportStartTick + aznumeric_cast(percentWindow * overallTickDelta); - const auto newEndTick = m_viewportEndTick - aznumeric_cast((1-percentWindow) * overallTickDelta); - - // Avoid zooming too much, start tick should always be less than end tick - if (newStartTick < newEndTick) - { - m_viewportStartTick = newStartTick; - m_viewportEndTick = newEndTick; - } - } - } - } - ImGui::EndChild(); - } - - inline void ImGuiCpuProfiler::CacheCpuTimingStatistics() - { - using namespace AZ::Statistics; - - m_cpuTimingStatisticsWhenPause.clear(); - if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) - { - auto& rhiMetrics = statsProfiler->GetProfiler(AZ_CRC_CE("RHI")); - - const NamedRunningStatistic* frameTimeMetric = rhiMetrics.GetStatistic(AZ_CRC_CE("Frame to Frame Time")); - if (frameTimeMetric) - { - m_frameToFrameTime = static_cast(frameTimeMetric->GetMostRecentSample()); - } - - AZStd::vector statistics; - rhiMetrics.GetStatsManager().GetAllStatistics(statistics); - - for (NamedRunningStatistic* stat : statistics) - { - m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample() }); - stat->Reset(); - } - } - } - - inline void ImGuiCpuProfiler::CollectFrameData() - { - // We maintain separate datastores for the visualizer and the statistical view because they require different - // data formats - one grouped by thread ID versus the other organized by group + region. Since the statistical - // view is only holding data from the last frame, the memory overhead is minimal and gives us a faster redraw - // compared to if we needed to transform the visualizer's data into the statistical format every frame. - - // Get the latest TimeRegionMap - const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap(); - - m_viewportStartTick = AZStd::numeric_limits::max(); - m_viewportEndTick = AZStd::numeric_limits::lowest(); - - // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame - for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap) - { - const size_t threadIdHashed = AZStd::hash{}(threadId); - // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) - if (singleThreadRegionMap.size() == 0) - { - continue; - } - - // Now focus on just the data for the current thread - AZStd::vector newVisualizerData; - newVisualizerData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation - for (const auto& [regionName, regionVec] : singleThreadRegionMap) - { - for (const TimeRegion& region : regionVec) - { - newVisualizerData.push_back(region); // Copies - - // Also update the statistical view's data - const AZStd::string& groupName = region.m_groupRegionName.m_groupName; - - if (!m_groupRegionMap[groupName].contains(regionName)) - { - m_groupRegionMap[groupName][regionName].m_groupName = groupName; - m_groupRegionMap[groupName][regionName].m_regionName = regionName; - m_tableData.push_back(&m_groupRegionMap[groupName][regionName]); - } - - m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed); - } - } - - // Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw) - // since we can binary search by start tick. - AZStd::sort( - newVisualizerData.begin(), newVisualizerData.end(), - [](const TimeRegion& lhs, const TimeRegion& rhs) - { - return lhs.m_startTick < rhs.m_startTick; - }); - - // Use the latest frame's data as the new bounds of the viewport - m_viewportStartTick = AZStd::min(newVisualizerData.front().m_startTick, m_viewportStartTick); - m_viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, m_viewportEndTick); - - m_savedRegionCount += newVisualizerData.size(); - - // Move onto the end of the current thread's saved data, sorted order maintained - AZStd::vector& savedDataVec = m_savedData[threadIdHashed]; - savedDataVec.insert( - savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end())); - } - } - - inline void ImGuiCpuProfiler::CullFrameData() - { - const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - m_frameToFrameTime * m_framesToCollect; - - // Remove old frame boundary data - auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick); - m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr); - - // Remove old region data for each thread - for (auto& [threadId, savedRegions] : m_savedData) - { - AZStd::size_t sizeBeforeRemove = savedRegions.size(); - - // Early out to avoid the linear erase_if call - if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick) - { - continue; - } - - // Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right - // for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf. - AZStd::erase_if( - savedRegions, - [deleteBeforeTick](const TimeRegion& region) - { - return region.m_startTick < deleteBeforeTick; - }); - - m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); - } - - // Remove any threads from the top-level map that no longer hold data - AZStd::erase_if( - m_savedData, - [](const auto& singleThreadDataEntry) - { - return singleThreadDataEntry.second.empty(); - }); - } - - inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow) - { - // Don't draw anything if the user is searching for regions and this block doesn't pass the filter - if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName)) - { - return; - } - - float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); - - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick); - const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick); - - if (endPixel - startPixel < 0.5f) - { - return; - } - - const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1}; - const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; - - const ImU32 blockColor = GetBlockColor(block); - - drawList->AddRectFilled(startPoint, endPoint, blockColor, 0); - drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f); - drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f); - - // Draw the region name if possible - // If the block's current width is too small, we skip drawing the label. - const float regionPixelWidth = endPixel - startPixel; - const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em) - if (regionPixelWidth > maxCharWidth) // We can draw at least one character - { - const AZStd::string label = - AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); - const float textWidth = ImGui::CalcTextSize(label.c_str()).x; - - if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. - { - const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y }; - - // NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI) - // so we must adjust for the scale manually. - const float scaleFactor = ImGui::GetIO().FontGlobalScale; - const float fontSize = ImGui::GetFont()->FontSize * scaleFactor; - - ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); - } - else // We have enough space to draw the entire label, draw and center text. - { - const float remainingWidth = regionPixelWidth - textWidth; - const float offset = remainingWidth * .5f; - - drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); - } - } - - // Tooltip and block highlighting - if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) - { - // Go to the statistics view when a region is clicked - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - { - m_enableVisualizer = false; - const auto newFilter = AZStd::string(block.m_groupRegionName.m_regionName); - m_timedRegionFilter = ImGuiTextFilter(newFilter.c_str()); - m_timedRegionFilter.Build(); - } - // Hovering outline - drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); - - ImGui::BeginTooltip(); - ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); - ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); - ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); - ImGui::EndTooltip(); - } - } - - inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) - { - // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers - const GroupRegionName& key = block.m_groupRegionName; - if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit - { - return ImGui::GetColorU32(iter->second); - } - - // Cache miss, generate a new random color - AZ::SimpleLcgRandom rand(aznumeric_cast(AZStd::GetTimeNowTicks())); - const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); - const ImVec4 randomColor = {r, g, b, .8}; - m_regionColorMap.emplace(key, randomColor); - return ImGui::GetColorU32(randomColor); - } - - inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth) - { - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - - auto [wx, wy] = ImGui::GetWindowPos(); - wy -= ImGui::GetScrollY(); - const float windowWidth = ImGui::GetWindowWidth(); - const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; - - ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); - } - - inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, size_t threadId) - { - auto [wx, wy] = ImGui::GetWindowPos(); - wy -= ImGui::GetScrollY(); - const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId); - - ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); - } - - inline void ImGuiCpuProfiler::DrawFrameBoundaries() - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - const float wy = ImGui::GetWindowPos().y; - const float windowHeight = ImGui::GetWindowHeight(); - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - - // End ticks are sorted in increasing order, find the first frame bound to draw - auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); - - while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick) - { - const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr, m_viewportStartTick, m_viewportEndTick); - drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red); - ++endTickItr; - } - } - - inline void ImGuiCpuProfiler::DrawRuler() - { - // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines - auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); - auto nextFrameBoundaryItr = lastFrameBoundaryItr; - if (lastFrameBoundaryItr != m_frameEndTicks.begin()) - { - --lastFrameBoundaryItr; - } - - const auto [wx, wy] = ImGui::GetWindowPos(); - ImDrawList* drawList = ImGui::GetWindowDrawList(); - - while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick) - { - const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr; - const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr; - if (lastFrameBoundaryTick > m_viewportEndTick) - { - break; - } - - const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); - - const AZStd::string label = - AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); - const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; - - // The label can fit between the two boundaries, center it and draw - if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel) - { - const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2; - const float textBeginPixel = lastFrameBoundaryPixel + offset; - const float textEndPixel = textBeginPixel + labelWidth; - - const float verticalOffset = (ImGui::GetWindowHeight() - ImGui::GetFontSize()) / 2; - - // Execution time label - drawList->AddText({ textBeginPixel, wy + verticalOffset }, IM_COL32_WHITE, label.c_str()); - - // Left side - drawList->AddLine( - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2}, - IM_COL32_WHITE); - - // Right side - drawList->AddLine( - { textEndPixel, wy + ImGui::GetWindowHeight()/2 }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 }, - IM_COL32_WHITE); - } - else // Cannot fit inside, just draw a line between the two boundaries - { - drawList->AddLine( - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, - IM_COL32_WHITE); - } - - // Left bound - drawList->AddLine( - { lastFrameBoundaryPixel, wy }, - { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, - IM_COL32_WHITE); - - // Right bound - drawList->AddLine( - { nextFrameBoundaryPixel, wy }, - { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, - IM_COL32_WHITE); - - lastFrameBoundaryItr = nextFrameBoundaryItr; - ++nextFrameBoundaryItr; - } - } - - inline void ImGuiCpuProfiler::DrawFrameTimeHistogram() - { - ImDrawList* drawList = ImGui::GetWindowDrawList(); - const auto [wx, wy] = ImGui::GetWindowPos(); - const ImU32 orange = ImGui::GetColorU32({ 1, .7, 0, 1 }); - const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); - - const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); - const AZStd::sys_time_t viewportCenter = m_viewportEndTick - (m_viewportEndTick - m_viewportStartTick) / 2; - const AZStd::sys_time_t leftHistogramBound = viewportCenter - ticksPerSecond; - const AZStd::sys_time_t rightHistogramBound = viewportCenter + ticksPerSecond; - - // Draw frame limit lines - drawList->AddLine( - { wx, wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, - { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, - orange); - - drawList->AddLine( - { wx, wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, - { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, - red); - - - // Draw viewport bound rectangle - const float leftViewportPixel = ConvertTickToPixelSpace(m_viewportStartTick, leftHistogramBound, rightHistogramBound); - const float rightViewportPixel = ConvertTickToPixelSpace(m_viewportEndTick, leftHistogramBound, rightHistogramBound); - const ImVec2 topLeftPos = { leftViewportPixel, wy }; - const ImVec2 botRightPos = { rightViewportPixel, wy + ImGui::GetWindowHeight() }; - const ImU32 gray = ImGui::GetColorU32({ 1, 1, 1, .3 }); - drawList->AddRectFilled(topLeftPos, botRightPos, gray); - - // Find the first onscreen frame execution time - auto frameEndTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), leftHistogramBound); - if (frameEndTickItr != m_frameEndTicks.begin()) - { - --frameEndTickItr; - } - - // Since we only store the frame end ticks, we must calculate the execution times on the fly by comparing pairs of elements. - AZStd::sys_time_t lastFrameEndTick = *frameEndTickItr; - while (*frameEndTickItr < rightHistogramBound && ++frameEndTickItr != m_frameEndTicks.end()) - { - const AZStd::sys_time_t frameEndTick = *frameEndTickItr; - - const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); - const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); - - const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; - const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; - - ImU32 lineColor = ImGui::GetColorU32({ .3, .3, .3, 1 }); // Gray - if (frameTimeMs > HighFrameTimeLimit) - { - lineColor = ImGui::GetColorU32({1, 0, 0, 1}); // Red - } - else if (frameTimeMs > MediumFrameTimeLimit) - { - lineColor = ImGui::GetColorU32({1, .7, 0, 1}); // Orange - } - - drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); - - lastFrameEndTick = frameEndTick; - } - - // Handle input - ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); - ImGuiIO& io = ImGui::GetIO(); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) - { - const float mousePixelX = io.MousePos.x; - const float percentWindow = (mousePixelX - wx) / ImGui::GetWindowWidth(); - const AZStd::sys_time_t newViewportCenterTick = leftHistogramBound + - aznumeric_cast((rightHistogramBound - leftHistogramBound) * percentWindow); - - const AZStd::sys_time_t viewportWidth = GetViewportTickWidth(); - m_viewportEndTick = newViewportCenterTick + viewportWidth / 2; - m_viewportStartTick = newViewportCenterTick - viewportWidth / 2; - } - } - - inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const - { - return m_viewportEndTick - m_viewportStartTick; - } - - inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const - { - const float wx = ImGui::GetWindowPos().x; - const float tickSpaceShifted = aznumeric_cast(tick - leftBound); // This will be close to zero, so FP inaccuracy should not be too bad - const float tickSpaceNormalized = tickSpaceShifted / (rightBound - leftBound); - const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx; - return pixelSpace; - } - - // System tick bus overrides - inline void ImGuiCpuProfiler::OnSystemTick() - { - if (m_paused) - { - SystemTickBus::Handler::BusDisconnect(); - } - else - { - m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); - - for (auto& [groupName, regionMap] : m_groupRegionMap) - { - for (auto& [regionName, row] : regionMap) - { - row.ResetPerFrameStatistics(); - } - } - } - } - - // ---- TableRow impl ---- - - inline void TableRow::RecordRegion(const AZ::RHI::CachedTimeRegion& region, size_t threadId) - { - const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick; - - // Update per frame statistics - ++m_invocationsLastFrame; - m_executingThreads.insert(threadId); - m_lastFrameTotalTicks += deltaTime; - m_maxTicks = AZStd::max(m_maxTicks, deltaTime); - - // Update aggregate statistics - m_runningAverageTicks = - aznumeric_cast((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1)); - ++m_invocationsTotal; - } - - inline void TableRow::ResetPerFrameStatistics() - { - m_invocationsLastFrame = 0; - m_executingThreads.clear(); - m_lastFrameTotalTicks = 0; - m_maxTicks = 0; - } - - inline AZStd::string TableRow::GetExecutingThreadsLabel() const - { - auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size()); - for (const auto& threadId : m_executingThreads) - { - threadString.append(AZStd::string::format("Thread: %zu\n", threadId)); - } - return threadString; - } - } // namespace Render -} // namespace AZ diff --git a/Gems/Atom/Utils/Code/atom_utils_files.cmake b/Gems/Atom/Utils/Code/atom_utils_files.cmake index dd4654b738..c11c2e294d 100644 --- a/Gems/Atom/Utils/Code/atom_utils_files.cmake +++ b/Gems/Atom/Utils/Code/atom_utils_files.cmake @@ -9,8 +9,6 @@ set(FILES Include/Atom/Utils/DdsFile.h Include/Atom/Utils/ImageComparison.h - Include/Atom/Utils/ImGuiCpuProfiler.h - Include/Atom/Utils/ImGuiCpuProfiler.inl Include/Atom/Utils/ImGuiCullingDebug.h Include/Atom/Utils/ImGuiCullingDebug.inl Include/Atom/Utils/ImGuiGpuProfiler.h diff --git a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp index b369ab2ee2..3f64e33e22 100644 --- a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp +++ b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.cpp @@ -84,10 +84,6 @@ namespace AtomImGuiTools { m_imguiGpuProfiler.Draw(m_showGpuProfiler, AZ::RPI::PassSystemInterface::Get()->GetRootPass().get()); } - if (m_showCpuProfiler) - { - m_imguiCpuProfiler.Draw(m_showCpuProfiler); - } if (m_showTransientAttachmentProfiler) { auto* transientStats = AZ::RHI::RHISystemInterface::Get()->GetTransientAttachmentStatistics(); @@ -108,12 +104,6 @@ namespace AtomImGuiTools { ImGui::MenuItem("Pass Viewer", "", &m_showPassTree); ImGui::MenuItem("Gpu Profiler", "", &m_showGpuProfiler); - if (ImGui::MenuItem("Cpu Profiler", "", &m_showCpuProfiler)) - { - AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( - AZ::RHI::FrameSchedulerStatisticsFlags::GatherCpuTimingStatistics, m_showCpuProfiler); - AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(m_showCpuProfiler); - } if (ImGui::MenuItem("Transient Attachment Profiler", "", &m_showTransientAttachmentProfiler)) { AZ::RHI::RHISystemInterface::Get()->ModifyFrameSchedulerStatisticsFlags( diff --git a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h index 890322bda7..3df3fc7fe5 100644 --- a/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h +++ b/Gems/AtomLyIntegration/AtomImGuiTools/Code/Source/AtomImGuiToolsSystemComponent.h @@ -15,7 +15,6 @@ #if defined(IMGUI_ENABLED) #include #include -#include #include #include #include @@ -63,9 +62,6 @@ namespace AtomImGuiTools AZ::Render::ImGuiGpuProfiler m_imguiGpuProfiler; bool m_showGpuProfiler = false; - AZ::Render::ImGuiCpuProfiler m_imguiCpuProfiler; - bool m_showCpuProfiler = false; - AZ::Render::ImGuiTransientAttachmentProfiler m_imguiTransientAttachmentProfiler; bool m_showTransientAttachmentProfiler = false; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.cpp index 1718dde5d4..2bf428bd2d 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.cpp @@ -216,11 +216,17 @@ namespace AZ using namespace LyIntegration; ThumbnailerRequestsBus::Broadcast( - &ThumbnailerRequests::RegisterThumbnailProvider, MAKE_TCACHE(SharedThumbnailCache), - ThumbnailContext::DefaultContext); + &ThumbnailerRequests::RegisterThumbnailProvider, MAKE_TCACHE(SharedThumbnailCache), ThumbnailContext::DefaultContext); + + if (!m_thumbnailRenderer) + { + m_thumbnailRenderer = AZStd::make_unique(); + } - m_renderer = AZStd::make_unique(); - m_previewerFactory = AZStd::make_unique(); + if (!m_previewerFactory) + { + m_previewerFactory = AZStd::make_unique(); + } } void EditorCommonFeaturesSystemComponent::TeardownThumbnails() @@ -232,7 +238,7 @@ namespace AZ &ThumbnailerRequests::UnregisterThumbnailProvider, SharedThumbnailCache::ProviderName, ThumbnailContext::DefaultContext); - m_renderer.reset(); + m_thumbnailRenderer.reset(); m_previewerFactory.reset(); } } // namespace Render diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.h index 8021770873..82bb93a808 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/EditorCommonFeaturesSystemComponent.h @@ -78,7 +78,7 @@ namespace AZ AZStd::string m_atomLevelDefaultAssetPath{ "LevelAssets/default.slice" }; float m_envProbeHeight{ 200.0f }; - AZStd::unique_ptr m_renderer; + AZStd::unique_ptr m_thumbnailRenderer; AZStd::unique_ptr m_previewerFactory; }; } // namespace Render diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp index 5df17a5478..746b1ff7e5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp @@ -95,6 +95,7 @@ namespace AZ void EditorMaterialSystemComponent::Activate() { + AZ::EntitySystemBus::Handler::BusConnect(); EditorMaterialSystemComponentNotificationBus::Handler::BusConnect(); EditorMaterialSystemComponentRequestBus::Handler::BusConnect(); AzToolsFramework::AssetBrowser::AssetBrowserInteractionNotificationBus::Handler::BusConnect(); @@ -106,6 +107,7 @@ namespace AZ void EditorMaterialSystemComponent::Deactivate() { + AZ::EntitySystemBus::Handler::BusDisconnect(); EditorMaterialSystemComponentNotificationBus::Handler::BusDisconnect(); EditorMaterialSystemComponentRequestBus::Handler::BusDisconnect(); AzToolsFramework::AssetBrowser::AssetBrowserInteractionNotificationBus::Handler::BusDisconnect(); @@ -185,7 +187,7 @@ namespace AZ materialAssignmentId); previewRenderer->AddCaptureRequest( - { 128, + { MaterialPreviewResolution, AZStd::make_shared( previewRenderer->GetScene(), previewRenderer->GetView(), previewRenderer->GetEntityContextId(), AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultModelPath), materialAssetId, @@ -220,11 +222,17 @@ namespace AZ return QPixmap(); } + void EditorMaterialSystemComponent::OnEntityDestroyed(const AZ::EntityId& entityId) + { + m_materialPreviews.erase(entityId); + } + void EditorMaterialSystemComponent::OnRenderMaterialPreviewComplete( [[maybe_unused]] const AZ::EntityId& entityId, [[maybe_unused]] const AZ::Render::MaterialAssignmentId& materialAssignmentId, [[maybe_unused]] const QPixmap& pixmap) { + PurgePreviews(); m_materialPreviews[entityId][materialAssignmentId] = pixmap; } @@ -281,5 +289,19 @@ namespace AZ } return AzToolsFramework::AssetBrowser::SourceFileDetails(); } + + void EditorMaterialSystemComponent::PurgePreviews() + { + size_t materialPreviewCount = 0; + for (const auto& materialPreviewPair : m_materialPreviews) + { + materialPreviewCount += materialPreviewPair.second.size(); + } + + if (materialPreviewCount > MaterialPreviewLimit) + { + m_materialPreviews.clear(); + } + } } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.h index 6fce538ead..38e12a7c4e 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,7 @@ namespace AZ //! System component that manages launching and maintaining connections with the material editor. class EditorMaterialSystemComponent final : public AZ::Component + , public AZ::EntitySystemBus::Handler , public EditorMaterialSystemComponentNotificationBus::Handler , public EditorMaterialSystemComponentRequestBus::Handler , public AzToolsFramework::AssetBrowser::AssetBrowserInteractionNotificationBus::Handler @@ -54,9 +56,12 @@ namespace AZ QPixmap GetRenderedMaterialPreview( const AZ::EntityId& entityId, const AZ::Render::MaterialAssignmentId& materialAssignmentId) const override; + // AZ::EntitySystemBus::Handler overrides... + void OnEntityDestroyed(const AZ::EntityId& entityId) override; + //! EditorMaterialSystemComponentNotificationBus::Handler overrides... void OnRenderMaterialPreviewComplete( - const AZ::EntityId& entityId, const AZ::Render::MaterialAssignmentId& materialAssignmentId, const QPixmap& pixmap)override; + const AZ::EntityId& entityId, const AZ::Render::MaterialAssignmentId& materialAssignmentId, const QPixmap& pixmap) override; //! AssetBrowserInteractionNotificationBus::Handler overrides... AzToolsFramework::AssetBrowser::SourceFileDetails GetSourceFileDetails(const char* fullSourceFileName) override; @@ -68,9 +73,13 @@ namespace AZ // AztoolsFramework::EditorEvents::Bus::Handler overrides... void NotifyRegisterViews() override; + void PurgePreviews(); + QAction* m_openMaterialEditorAction = nullptr; AZStd::unique_ptr m_materialBrowserInteractions; AZStd::unordered_map> m_materialPreviews; + static constexpr const size_t MaterialPreviewLimit = 100; + static constexpr const int MaterialPreviewResolution = 128; }; } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp index 49c8275141..2d8f83a32a 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp @@ -7,6 +7,10 @@ */ #include +#include +#include +#include +#include namespace AZ { @@ -18,7 +22,10 @@ namespace AZ if (AZ::SerializeContext* serializeContext = azrtti_cast(context)) { - serializeContext->Class()->Version(1); + serializeContext->Class() + ->Version(2) + ->Field("generatedLut", &EditorHDRColorGradingComponent::m_generatedLutAbsolutePath) + ; if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -31,6 +38,20 @@ namespace AZ ->Attribute(Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) ->Attribute(Edit::Attributes::AutoExpand, true) ->Attribute(Edit::Attributes::HelpPageURL, "https://") // [TODO ATOM-2672][PostFX] need to create page for PostProcessing. + ->ClassElement(AZ::Edit::ClassElements::Group, "LUT Generation") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->UIElement(AZ::Edit::UIHandlers::Button, "Generate LUT", "Generates a LUT from the scene's enabled color grading blend.") + ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "") + ->Attribute(AZ::Edit::Attributes::ButtonText, "Generate LUT") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHDRColorGradingComponent::GenerateLut) + ->DataElement(AZ::Edit::UIHandlers::MultiLineEdit, &EditorHDRColorGradingComponent::m_generatedLutAbsolutePath, "Generated LUT Path", "Generated LUT Path") + ->Attribute(AZ::Edit::Attributes::ReadOnly, true) + ->Attribute(AZ::Edit::Attributes::Visibility, &EditorHDRColorGradingComponent::GetGeneratedLutVisibilitySettings) + ->UIElement(AZ::Edit::UIHandlers::Button, "Activate LUT", "Use the generated LUT asset in a Look Modification component") + ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "") + ->Attribute(AZ::Edit::Attributes::ButtonText, "Activate LUT") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHDRColorGradingComponent::ActivateLut) + ->Attribute(AZ::Edit::Attributes::Visibility, &EditorHDRColorGradingComponent::GetGeneratedLutVisibilitySettings) ; editContext->Class( @@ -47,6 +68,9 @@ namespace AZ "Enable HDR color grading.") ->ClassElement(AZ::Edit::ClassElements::Group, "Color Adjustment") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_colorAdjustmentWeight, "Weight", "Weight of color adjustments") + ->Attribute(Edit::Attributes::Min, 0.0f) + ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_colorGradingExposure, "Exposure", "Exposure Value") ->Attribute(Edit::Attributes::Min, AZStd::numeric_limits::lowest()) ->Attribute(Edit::Attributes::Max, AZStd::numeric_limits::max()) @@ -66,10 +90,13 @@ namespace AZ ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_colorGradingFilterMultiply, "Filter Multiply", "Filter Multiply Value") ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 1.0f) - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_colorFilterSwatch, "Color Filter Swatch", "Color Filter Swatch Value") + ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_colorFilterSwatch, "Filter Swatch", "Color Filter Swatch Value") ->ClassElement(AZ::Edit::ClassElements::Group, "White Balance") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceWeight, "Weight", "Weight of white balance") + ->Attribute(Edit::Attributes::Min, 0.0f) + ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceKelvin, "Temperature", "Temperature in Kelvin") ->Attribute(Edit::Attributes::Min, 1000.0f) ->Attribute(Edit::Attributes::Max, 40000.0f) @@ -79,14 +106,14 @@ namespace AZ ->ClassElement(AZ::Edit::ClassElements::Group, "Split Toning") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_splitToneWeight, "Split Tone Weight", "Modulates the split toning effect.") + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_splitToneWeight, "Weight", "Modulates the split toning effect.") ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 1.0f) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_splitToneBalance, "Split Tone Balance", "Split Tone Balance Value") - ->Attribute(Edit::Attributes::Min, 0.0f) + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_splitToneBalance, "Balance", "Split Tone Balance Value") + ->Attribute(Edit::Attributes::Min, -1.0f) ->Attribute(Edit::Attributes::Max, 1.0f) - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_splitToneShadowsColor, "Split Tone Shadows Color", "Split Tone Shadows Color") - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_splitToneHighlightsColor, "Split Tone Highlights Color", "Split Tone Highlights Color") + ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_splitToneShadowsColor, "Shadows Color", "Split Tone Shadows Color") + ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_splitToneHighlightsColor, "Highlights Color", "Split Tone Highlights Color") ->ClassElement(AZ::Edit::ClassElements::Group, "Channel Mixing") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) @@ -99,33 +126,52 @@ namespace AZ ->ClassElement(AZ::Edit::ClassElements::Group, "Shadow Midtones Highlights") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhWeight, "SMH Weight", "Modulates the SMH effect.") + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhWeight, "Weight", "Modulates the SMH effect.") ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 1.0f) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsStart, "SMH Shadows Start", "SMH Shadows Start Value") + ->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) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsEnd, "SMH Shadows End", "SMH Shadows End Value") + ->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) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsStart, "SMH Highlights Start", "SMH Highlights Start Value") + ->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) - ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsEnd, "SMH Highlights End", "SMH Highlights End Value") + ->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) - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhShadowsColor, "SMH Shadows Color", "SMH Shadows Color") - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhMidtonesColor, "SMH Midtones Color", "SMH Midtones Color") - ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhHighlightsColor, "SMH Highlights Color", "SMH Highlights Color") + ->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") ->ClassElement(AZ::Edit::ClassElements::Group, "Final Adjustment") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_finalAdjustmentWeight, "Weight", "Weight of final adjustments") + ->Attribute(Edit::Attributes::Min, 0.0f) + ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_colorGradingHueShift, "Hue Shift", "Hue Shift Value") ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_colorGradingPostSaturation, "Post Saturation", "Post Saturation Value") ->Attribute(Edit::Attributes::Min, -100.0f) ->Attribute(Edit::Attributes::Max, 100.0f) + + ->ClassElement(AZ::Edit::ClassElements::Group, "LUT Generation") + ->DataElement(AZ::Edit::UIHandlers::ComboBox, &HDRColorGradingComponentConfig::m_lutResolution, "LUT Resolution", "Resolution of generated LUT") + ->EnumAttribute(LutResolution::Lut16x16x16, "16x16x16") + ->EnumAttribute(LutResolution::Lut32x32x32, "32x32x32") + ->EnumAttribute(LutResolution::Lut64x64x64, "64x64x64") + ->DataElement(Edit::UIHandlers::ComboBox, &HDRColorGradingComponentConfig::m_shaperPresetType, + "Shaper Type", "Shaper Type.") + ->EnumAttribute(ShaperPresetType::None, "None") + ->EnumAttribute(ShaperPresetType::LinearCustomRange, "Linear Custom Range") + ->EnumAttribute(ShaperPresetType::Log2_48Nits, "Log2 48 nits") + ->EnumAttribute(ShaperPresetType::Log2_1000Nits, "Log2 1000 nits") + ->EnumAttribute(ShaperPresetType::Log2_2000Nits, "Log2 2000 nits") + ->EnumAttribute(ShaperPresetType::Log2_4000Nits, "Log2 4000 nits") + ->EnumAttribute(ShaperPresetType::Log2CustomRange, "Log2 Custom Range") + ->EnumAttribute(ShaperPresetType::PqSmpteSt2084, "PQ (SMPTE ST 2084)") ; } } @@ -136,6 +182,120 @@ namespace AZ { } + void EditorHDRColorGradingComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + { + if (m_waitOneFrame) + { + m_waitOneFrame = false; + return; + } + + const char* LutAttachment = "LutOutput"; + const AZStd::vector LutGenerationPassHierarchy{ "LutGenerationPass" }; + + char resolvedOutputFilePath[AZ_MAX_PATH_LEN] = { 0 }; + AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(m_currentTiffFilePath.c_str(), resolvedOutputFilePath, AZ_MAX_PATH_LEN); + + AZStd::string lutGenerationCacheFolder; + AzFramework::StringFunc::Path::GetFolderPath(resolvedOutputFilePath, lutGenerationCacheFolder); + AZ::IO::SystemFile::CreateDir(lutGenerationCacheFolder.c_str()); + + bool startedCapture = false; + AZ::Render::FrameCaptureRequestBus::BroadcastResult( + startedCapture, + &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachment, + LutGenerationPassHierarchy, + AZStd::string(LutAttachment), + m_currentTiffFilePath, + AZ::RPI::PassAttachmentReadbackOption::Output); + + if (startedCapture) + { + AZ::TickBus::Handler::BusDisconnect(); + AZ::Render::FrameCaptureNotificationBus::Handler::BusConnect(); + } + } + + void EditorHDRColorGradingComponent::OnCaptureFinished([[maybe_unused]] AZ::Render::FrameCaptureResult result, [[maybe_unused]]const AZStd::string& info) + { + char resolvedInputFilePath[AZ_MAX_PATH_LEN] = { 0 }; + AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(m_currentTiffFilePath.c_str(), resolvedInputFilePath, AZ_MAX_PATH_LEN); + char resolvedOutputFilePath[AZ_MAX_PATH_LEN] = { 0 }; + AZ::IO::FileIOBase::GetDirectInstance()->ResolvePath(m_currentLutFilePath.c_str(), resolvedOutputFilePath, AZ_MAX_PATH_LEN); + + AZStd::string lutGenerationFolder; + AzFramework::StringFunc::Path::GetFolderPath(resolvedOutputFilePath, lutGenerationFolder); + AZ::IO::SystemFile::CreateDir(lutGenerationFolder.c_str()); + + AZStd::vector pythonArgs + { + "--i", resolvedInputFilePath, + "--o", resolvedOutputFilePath + }; + + AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast( + &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs, + TiffToAzassetPythonScriptPath, + pythonArgs); + + m_controller.m_configuration.m_generateLut = false; + m_controller.OnConfigChanged(); + + m_generatedLutAbsolutePath = resolvedOutputFilePath + AZStd::string(".azasset"); + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_EntireTree); + + AZ::Render::FrameCaptureNotificationBus::Handler::BusDisconnect(); + } + + void EditorHDRColorGradingComponent::GenerateLut() + { + // turn on lut generation pass + AZ::Uuid uuid = AZ::Uuid::CreateRandom(); + AZStd::string uuidString; + uuid.ToString(uuidString); + + m_currentTiffFilePath = AZStd::string::format(TempTiffFilePath, uuidString.c_str()); + m_currentLutFilePath = "@projectroot@/" + AZStd::string::format(GeneratedLutRelativePath, uuidString.c_str()); + + m_controller.SetGenerateLut(true); + m_controller.OnConfigChanged(); + + m_waitOneFrame = true; + + AZ::TickBus::Handler::BusConnect(); + } + + AZ::u32 EditorHDRColorGradingComponent::ActivateLut() + { + using namespace AzFramework::StringFunc::Path; + + AZStd::string entityName; + AZ::ComponentApplicationBus::BroadcastResult(entityName, &AZ::ComponentApplicationRequests::GetEntityName, GetEntityId()); + + AZStd::string filename; + GetFileName(m_generatedLutAbsolutePath.c_str(), filename); + AZStd::string assetRelativePath = "LutGeneration/" + filename + ".azasset"; + AZStd::vector pythonArgs + { + "--entityName", entityName, + "--assetRelativePath", assetRelativePath + }; + + AzToolsFramework::EditorPythonRunnerRequestBus::Broadcast( + &AzToolsFramework::EditorPythonRunnerRequestBus::Events::ExecuteByFilenameWithArgs, + ActivateLutAssetPythonScriptPath, + pythonArgs); + + return AZ::Edit::PropertyRefreshLevels::EntireTree; + } + + bool EditorHDRColorGradingComponent::GetGeneratedLutVisibilitySettings() + { + return !m_generatedLutAbsolutePath.empty(); + } + u32 EditorHDRColorGradingComponent::OnConfigurationChanged() { m_controller.OnConfigChanged(); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.h index 8eea568e3a..df3842d57a 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.h @@ -8,16 +8,25 @@ #pragma once +#include #include +#include #include namespace AZ { namespace Render { + static constexpr const char* const TempTiffFilePath{ "@usercache@/LutGeneration/SavedLut_%s.tiff" }; + static constexpr const char* const GeneratedLutRelativePath = { "LutGeneration/SavedLut_%s" }; + static constexpr const char* const TiffToAzassetPythonScriptPath{ "@engroot@/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/tiff_to_3dl_azasset.py" }; + static constexpr const char* const ActivateLutAssetPythonScriptPath{ "@engroot@/Gems/Atom/Feature/Common/Editor/Scripts/ColorGrading/activate_lut_asset.py" }; + class EditorHDRColorGradingComponent final : public AzToolsFramework::Components:: EditorComponentAdapter + , private TickBus::Handler + , private FrameCaptureNotificationBus::Handler { public: using BaseClass = AzToolsFramework::Components::EditorComponentAdapter; @@ -30,6 +39,23 @@ namespace AZ //! EditorRenderComponentAdapter overrides... AZ::u32 OnConfigurationChanged() override; + + private: + // AZ::TickBus overrides ... + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + + // FrameCaptureNotificationBus overrides ... + void OnCaptureFinished(AZ::Render::FrameCaptureResult result, const AZStd::string& info) override; + + void GenerateLut(); + AZ::u32 ActivateLut(); + bool GetGeneratedLutVisibilitySettings(); + + bool m_waitOneFrame = false; + AZStd::string m_currentTiffFilePath; + AZStd::string m_currentLutFilePath; + + AZStd::string m_generatedLutAbsolutePath; }; } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/HDRColorGradingComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/HDRColorGradingComponentController.cpp index c0f70ab145..fb5e792430 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/HDRColorGradingComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/HDRColorGradingComponentController.cpp @@ -52,6 +52,7 @@ namespace AZ void HDRColorGradingComponentController::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC_CE("HDRColorGradingService")); + incompatible.push_back(AZ_CRC_CE("LookModificationService")); } void HDRColorGradingComponentController::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/LookModification/LookModificationComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/LookModification/LookModificationComponentController.cpp index 4f321ed8b1..aa8c305e72 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/LookModification/LookModificationComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/LookModification/LookModificationComponentController.cpp @@ -49,12 +49,12 @@ namespace AZ void LookModificationComponentController::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { - provided.push_back(AZ_CRC("LookModificationService", 0x207b7539)); + provided.push_back(AZ_CRC_CE("LookModificationService")); } void LookModificationComponentController::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { - incompatible.push_back(AZ_CRC("LookModificationService", 0x207b7539)); + incompatible.push_back(AZ_CRC("LookModificationService")); } void LookModificationComponentController::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Camera_category.svg b/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Camera_category.svg new file mode 100644 index 0000000000..4c7ca1871b --- /dev/null +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Camera_category.svg @@ -0,0 +1,10 @@ + + + + Icons / Video Control / Video + Created with Sketch. + + + + + diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Resources.qrc b/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Resources.qrc new file mode 100644 index 0000000000..28ae322d5b --- /dev/null +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Assets/Icons/Resources.qrc @@ -0,0 +1,5 @@ + + + Camera_category.svg + + diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/CMakeLists.txt b/Gems/AtomLyIntegration/EMotionFXAtom/Code/CMakeLists.txt index cc71e4f237..bb829ebeec 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/CMakeLists.txt +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/CMakeLists.txt @@ -49,6 +49,7 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) NAME EMotionFX_Atom.Editor GEM_MODULE NAMESPACE Gem + AUTORCC FILES_CMAKE emotionfx_atom_editor_files.cmake INCLUDE_DIRECTORIES diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp index 52f857551c..faa033956a 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp @@ -181,6 +181,31 @@ namespace EMStudio ResetEnvironment(); } + AZ::Vector3 AnimViewportRenderer::GetCharacterCenter() const + { + AZ::Vector3 result = AZ::Vector3::CreateZero(); + if (!m_actorEntities.empty()) + { + // Find the actor instance and calculate the center from aabb. + AZ::Vector3 actorCenter = AZ::Vector3::CreateZero(); + EMotionFX::Integration::ActorComponent* actorComponent = + m_actorEntities[0]->FindComponent(); + EMotionFX::ActorInstance* actorInstance = actorComponent->GetActorInstance(); + if (actorInstance) + { + actorCenter += actorInstance->GetAabb().GetCenter(); + } + + // Just return the position of the first entity. + AZ::Transform worldTransform; + AZ::TransformBus::EventResult(worldTransform, m_actorEntities[0]->GetId(), &AZ::TransformBus::Events::GetWorldTM); + result = worldTransform.GetTranslation(); + result += actorCenter; + } + + return result; + } + void AnimViewportRenderer::ResetEnvironment() { // Reset environment diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.h index dd420c175c..1de4cbdb1c 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.h @@ -49,6 +49,9 @@ namespace EMStudio void Reinit(); + //! Return the center position of the existing objects. + AZ::Vector3 GetCharacterCenter() const; + private: // This function resets the light, camera and other environment settings. diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRequestBus.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRequestBus.h new file mode 100644 index 0000000000..03784c2158 --- /dev/null +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRequestBus.h @@ -0,0 +1,41 @@ +/* + * 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 EMStudio +{ + enum CameraViewMode + { + FRONT, + BACK, + TOP, + BOTTOM, + LEFT, + RIGHT, + DEFAULT + }; + + class AnimViewportRequests + : public AZ::EBusTraits + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + + //! Reset the camera to initial state. + virtual void ResetCamera() = 0; + + //! Set the camera view mode. + virtual void SetCameraViewMode(CameraViewMode mode) = 0; + }; + + using AnimViewportRequestBus = AZ::EBus; +} // namespace EMStudio diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.cpp new file mode 100644 index 0000000000..a47a773e10 --- /dev/null +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.cpp @@ -0,0 +1,60 @@ +/* + * 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 EMStudio +{ + AnimViewportToolBar::AnimViewportToolBar(QWidget* parent) + : QToolBar(parent) + { + AzQtComponents::ToolBar::addMainToolBarStyle(this); + + // Add the camera button + QToolButton* cameraButton = new QToolButton(this); + QMenu* cameraMenu = new QMenu(cameraButton); + + // Add the camera option + const AZStd::vector> cameraOptionNames = { + { CameraViewMode::FRONT, "Front" }, { CameraViewMode::BACK, "Back" }, { CameraViewMode::TOP, "Top" }, + { CameraViewMode::BOTTOM, "Bottom" }, { CameraViewMode::LEFT, "Left" }, { CameraViewMode::RIGHT, "Right" }, + }; + + for (const auto& pair : cameraOptionNames) + { + CameraViewMode mode = pair.first; + cameraMenu->addAction( + pair.second.c_str(), + [mode]() + { + // Send the reset camera event. + AnimViewportRequestBus::Broadcast(&AnimViewportRequestBus::Events::SetCameraViewMode, mode); + }); + } + + cameraMenu->addSeparator(); + cameraMenu->addAction("Reset Camera", + []() + { + // Send the reset camera event. + AnimViewportRequestBus::Broadcast(&AnimViewportRequestBus::Events::ResetCamera); + }); + cameraButton->setMenu(cameraMenu); + cameraButton->setText("Camera Option"); + cameraButton->setPopupMode(QToolButton::InstantPopup); + cameraButton->setVisible(true); + cameraButton->setIcon(QIcon(":/EMotionFXAtom/Camera_category.svg")); + addWidget(cameraButton); + } +} // namespace EMStudio diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.h new file mode 100644 index 0000000000..23ef5fdcd8 --- /dev/null +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportToolBar.h @@ -0,0 +1,24 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#endif + +namespace EMStudio +{ + class AnimViewportToolBar : public QToolBar + { + public: + AnimViewportToolBar(QWidget* parent = nullptr); + ~AnimViewportToolBar() = default; + }; +} diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp index 6a6c9df901..2e05864adc 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp @@ -34,6 +34,23 @@ namespace EMStudio SetupCameras(); SetupCameraController(); + Reinit(); + + AnimViewportRequestBus::Handler::BusConnect(); + } + + AnimViewportWidget::~AnimViewportWidget() + { + AnimViewportRequestBus::Handler::BusDisconnect(); + } + + void AnimViewportWidget::Reinit(bool resetCamera) + { + if (resetCamera) + { + ResetCamera(); + } + m_renderer->Reinit(); } void AnimViewportWidget::SetupCameras() @@ -100,4 +117,42 @@ namespace EMStudio }); GetControllerList()->Add(controller); } + + void AnimViewportWidget::ResetCamera() + { + SetCameraViewMode(CameraViewMode::DEFAULT); + } + + void AnimViewportWidget::SetCameraViewMode([[maybe_unused]]CameraViewMode mode) + { + // Set the camera view mode. + const AZ::Vector3 targetPosition = m_renderer->GetCharacterCenter(); + AZ::Vector3 cameraPosition; + switch (mode) + { + case CameraViewMode::FRONT: + cameraPosition.Set(0.0f, CameraDistance, targetPosition.GetZ()); + break; + case CameraViewMode::BACK: + cameraPosition.Set(0.0f, -CameraDistance, targetPosition.GetZ()); + break; + case CameraViewMode::TOP: + cameraPosition.Set(0.0f, 0.0f, CameraDistance + targetPosition.GetZ()); + break; + case CameraViewMode::BOTTOM: + cameraPosition.Set(0.0f, 0.0f, -CameraDistance + targetPosition.GetZ()); + break; + case CameraViewMode::LEFT: + cameraPosition.Set(-CameraDistance, 0.0f, targetPosition.GetZ()); + break; + case CameraViewMode::RIGHT: + cameraPosition.Set(CameraDistance, 0.0f, targetPosition.GetZ()); + break; + case CameraViewMode::DEFAULT: + // The default view mode is looking from the top left of the character. + cameraPosition.Set(-CameraDistance, CameraDistance, CameraDistance + targetPosition.GetZ()); + break; + } + GetViewportContext()->SetCameraTransform(AZ::Transform::CreateLookAt(cameraPosition, targetPosition)); + } } // namespace EMStudio diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h index 7c2e085f84..6d708b0996 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h @@ -9,6 +9,7 @@ #include #include +#include namespace EMStudio { @@ -16,15 +17,25 @@ namespace EMStudio class AnimViewportWidget : public AtomToolsFramework::RenderViewportWidget + , private AnimViewportRequestBus::Handler { public: AnimViewportWidget(QWidget* parent = nullptr); + ~AnimViewportWidget() override; AnimViewportRenderer* GetAnimViewportRenderer() { return m_renderer.get(); } + void Reinit(bool resetCamera = true); + private: void SetupCameras(); void SetupCameraController(); + // AnimViewportRequestBus::Handler overrides + void ResetCamera(); + void SetCameraViewMode(CameraViewMode mode); + + static constexpr float CameraDistance = 2.0f; + AZStd::unique_ptr m_renderer; AZStd::shared_ptr m_rotateCamera; AZStd::shared_ptr m_translateCamera; diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp index 91a34e16cd..bc74592485 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AtomRenderPlugin.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -76,7 +77,7 @@ namespace EMStudio void AtomRenderPlugin::ReinitRenderer() { - m_animViewportWidget->GetAnimViewportRenderer()->Reinit(); + m_animViewportWidget->Reinit(); } bool AtomRenderPlugin::Init() @@ -88,6 +89,12 @@ namespace EMStudio verticalLayout->setSizeConstraint(QLayout::SetNoConstraint); verticalLayout->setSpacing(1); verticalLayout->setMargin(0); + + // Add the tool bar + AnimViewportToolBar* toolBar = new AnimViewportToolBar(m_innerWidget); + verticalLayout->addWidget(toolBar); + + // Add the viewport widget m_animViewportWidget = new AnimViewportWidget(m_innerWidget); verticalLayout->addWidget(m_animViewportWidget); diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/emotionfx_atom_editor_files.cmake b/Gems/AtomLyIntegration/EMotionFXAtom/Code/emotionfx_atom_editor_files.cmake index a88581099f..330a3c4af7 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/emotionfx_atom_editor_files.cmake +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/emotionfx_atom_editor_files.cmake @@ -7,6 +7,7 @@ # set(FILES + ../Assets/Icons/Resources.qrc Source/ActorModule.cpp Source/Editor/EditorSystemComponent.h Source/Editor/EditorSystemComponent.cpp @@ -18,4 +19,7 @@ set(FILES Tools/EMStudio/AnimViewportRenderer.cpp Tools/EMStudio/AnimViewportSettings.h Tools/EMStudio/AnimViewportSettings.cpp + Tools/EMStudio/AnimViewportToolBar.h + Tools/EMStudio/AnimViewportToolBar.cpp + Tools/EMStudio/AnimViewportRequestBus.h ) diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 7c8ce74f8e..7a317dc09c 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/MorphTargetCommands.cpp b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/MorphTargetCommands.cpp index 0a48952bfc..3ec4b9b894 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/MorphTargetCommands.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/MorphTargetCommands.cpp @@ -6,7 +6,6 @@ * */ -// include the required headers #include "MorphTargetCommands.h" #include "CommandManager.h" #include @@ -110,6 +109,12 @@ namespace CommandSystem actor = actorInstance->GetActor(); } + if (!actor) + { + AZ_Error("EMotionFX", false, "Cannot adjust morph target. Actor with ID %i cannot be found.", actorID); + return false; + } + // get the level of detail to work on const uint32 lodLevel = parameters.GetValueAsInt("lodLevel", this); diff --git a/Gems/EMotionFX/Code/EMotionFX/Pipeline/EMotionFXBuilder/EMotionFXBuilderComponent.cpp b/Gems/EMotionFX/Code/EMotionFX/Pipeline/EMotionFXBuilder/EMotionFXBuilderComponent.cpp index 85b2b0fe3d..43222f9538 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Pipeline/EMotionFXBuilder/EMotionFXBuilderComponent.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Pipeline/EMotionFXBuilder/EMotionFXBuilderComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include @@ -38,6 +40,8 @@ namespace EMotionFX s_EMotionFXAllocator = AZ::Environment::CreateVariable(EMotionFXAllocatorInitializer::EMotionFXAllocatorInitializerTag); // Initialize asset handlers. + m_assetHandlers.emplace_back(aznew EMotionFX::Integration::ActorAssetHandler); + m_assetHandlers.emplace_back(aznew EMotionFX::Integration::MotionAssetHandler); m_assetHandlers.emplace_back(aznew EMotionFX::Integration::MotionSetAssetBuilderHandler); m_assetHandlers.emplace_back(aznew EMotionFX::Integration::AnimGraphAssetBuilderHandler); @@ -45,9 +49,13 @@ namespace EMotionFX auto assetCatalog = AZ::Data::AssetCatalogRequestBus::FindFirstHandler(); if (assetCatalog) { + assetCatalog->EnableCatalogForAsset(azrtti_typeid()); + assetCatalog->EnableCatalogForAsset(azrtti_typeid()); assetCatalog->EnableCatalogForAsset(azrtti_typeid()); assetCatalog->EnableCatalogForAsset(azrtti_typeid()); + assetCatalog->AddExtension("actor"); // Actor + assetCatalog->AddExtension("motion"); // Motion assetCatalog->AddExtension("motionset"); // Motion set assetCatalog->AddExtension("animgraph"); // Anim graph } diff --git a/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Actor/ActorGroupExporter.cpp b/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Actor/ActorGroupExporter.cpp index 3d5e4fa413..baa2984d4f 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Actor/ActorGroupExporter.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Pipeline/RCExt/Actor/ActorGroupExporter.cpp @@ -8,9 +8,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -82,10 +84,19 @@ namespace EMotionFX AZStd::string metaDataString; if (Rule::MetaDataRule::LoadMetaData(actorGroup, metaDataString)) { + // Create a temporary actor asset as the commands use the actor manager to find the corresponding actor object + // and our actor can only be found as part of a registered actor asset. + const AZ::Data::AssetId actorAssetId = AZ::Data::AssetId(AZ::Uuid::CreateRandom()); + AZ::Data::Asset actorAsset = AZ::Data::AssetManager::Instance().CreateAsset(actorAssetId); + actorAsset.GetAs()->SetData(m_actor); + GetEMotionFX().GetActorManager()->RegisterActor(actorAsset); + if (!CommandSystem::MetaData::ApplyMetaDataOnActor(m_actor.get(), metaDataString)) { AZ_Error("EMotionFX", false, "Applying meta data to actor '%s' failed.", m_actor->GetName()); } + + GetEMotionFX().GetActorManager()->UnregisterActor(actorAsset->GetId()); } AZStd::shared_ptr physicsSetup; diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 909590a25d..b971e6a2ce 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -175,6 +175,11 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) PRIVATE AZ::AzTest Gem::Multiplayer.Static + AUTOGEN_RULES + *.AutoComponent.xml,AutoComponent_Header.jinja,$path/$fileprefix.AutoComponent.h + *.AutoComponent.xml,AutoComponent_Source.jinja,$path/$fileprefix.AutoComponent.cpp + *.AutoComponent.xml,AutoComponentTypes_Header.jinja,$path/AutoComponentTypes.h + *.AutoComponent.xml,AutoComponentTypes_Source.jinja,$path/AutoComponentTypes.cpp ) ly_add_googletest( NAME Gem::Multiplayer.Tests diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h index bcbea6542f..25fe473135 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -71,6 +71,8 @@ namespace Multiplayer void UpdateAutonomous(AZ::TimeMs deltaTimeMs); void UpdateBankedTime(AZ::TimeMs deltaTimeMs); + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); + using StateHistoryItem = AZStd::unique_ptr; AZStd::map m_predictiveStateHistory; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index 674dc6114d..f0ad3baf94 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -29,6 +29,8 @@ namespace Multiplayer , public NetworkHierarchyRequestBus::Handler { friend class NetworkHierarchyChildComponent; + friend class NetworkHierarchyRootComponentController; + friend class ServerToClientReplicationWindow; public: AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHierarchyRootComponent, s_networkHierarchyRootComponentConcreteUuid, Multiplayer::NetworkHierarchyRootComponentBase); @@ -57,6 +59,8 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} + bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); + protected: void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); @@ -99,4 +103,24 @@ namespace Multiplayer friend class HierarchyBenchmarkBase; }; + + + //! NetworkHierarchyRootComponentController + //! This is the network controller for NetworkHierarchyRootComponent. + //! Class provides the ability to process input for hierarchies. + class NetworkHierarchyRootComponentController final + : public NetworkHierarchyRootComponentControllerBase + { + public: + NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent); + + // NetworkHierarchyRootComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + Multiplayer::MultiplayerController::InputPriorityOrder GetInputOrder() const override; + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; } diff --git a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml index 1a7496a77d..194c05577d 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml @@ -9,6 +9,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml index 0f33e1f642..fbe6ff2135 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -4,11 +4,15 @@ Name="NetworkHierarchyRootComponent" Namespace="Multiplayer" OverrideComponent="true" - OverrideController="false" + OverrideController="true" OverrideInclude="Multiplayer/Components/NetworkHierarchyRootComponent.h" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + - + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 747f53d37b..4d0756d9a5 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace Multiplayer { @@ -211,7 +212,7 @@ namespace Multiplayer m_lastCorrectionSentTimeMs = currentTimeMs; AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); const AZ::HashValue32 localAuthorityHash = hashSerializer.GetHash(); @@ -233,7 +234,7 @@ namespace Multiplayer // only deserialize if we have data (for client/server profile/debug mismatches) if (correction.GetSize() > 0) { - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); } correction.Resize(serializer.GetSize()); @@ -313,7 +314,7 @@ namespace Multiplayer // Apply the correction AzNetworking::TrackChangedSerializer serializer(correction.GetBuffer(), static_cast(correction.GetSize())); - GetNetBindComponent()->SerializeEntityCorrection(serializer); + SerializeEntityCorrection(serializer); GetNetBindComponent()->NotifyCorrection(); #ifndef AZ_RELEASE_BUILD @@ -325,7 +326,7 @@ namespace Multiplayer { // Read out state values AzNetworking::StringifySerializer serverValues; - GetNetBindComponent()->SerializeEntityCorrection(serverValues); + SerializeEntityCorrection(serverValues); PrintCorrectionDifferences(*iter->second, serverValues); } else @@ -452,7 +453,7 @@ namespace Multiplayer // Generate a hash based on the current client predicted states AzNetworking::HashSerializer hashSerializer; - GetNetBindComponent()->SerializeEntityCorrection(hashSerializer); + SerializeEntityCorrection(hashSerializer); // Save this input and discard move history outside our client rewind window m_inputHistory.PushBack(input); @@ -480,7 +481,7 @@ namespace Multiplayer { m_predictiveStateHistory.erase(m_predictiveStateHistory.begin()); } - GetNetBindComponent()->SerializeEntityCorrection(*inputHistory); + SerializeEntityCorrection(*inputHistory); m_predictiveStateHistory.emplace(m_clientInputId, AZStd::move(inputHistory)); } #endif @@ -493,6 +494,18 @@ namespace Multiplayer } } + bool LocalPredictionPlayerInputComponentController::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = GetNetBindComponent()->SerializeEntityCorrection(serializer); + + NetworkHierarchyRootComponent* hierarchyComponent = GetParent().GetNetworkHierarchyRootComponent(); + if (result && hierarchyComponent) + { + result = hierarchyComponent->SerializeEntityCorrection(serializer); + } + return result; + } + void LocalPredictionPlayerInputComponentController::UpdateBankedTime(AZ::TimeMs deltaTimeMs) { const double deltaTime = static_cast(deltaTimeMs) / 1000.0; diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 4a5e9ce8d2..e45389b7d9 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -326,4 +326,150 @@ namespace Multiplayer RebuildHierarchy(); } } + + NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent) + : NetworkHierarchyRootComponentControllerBase(parent) + { + + } + + void NetworkHierarchyRootComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + + } + + void NetworkHierarchyRootComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + + } + + Multiplayer::MultiplayerController::InputPriorityOrder NetworkHierarchyRootComponentController::GetInputOrder() const + { + return Multiplayer::MultiplayerController::InputPriorityOrder::SubEntities; + } + + void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime) + { + NetworkHierarchyRootComponent& component = GetParent(); + if(!component.IsHierarchicalRoot()) + { + return; + } + + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + const AZStd::vector& entities = component.m_hierarchicalEntities; + + auto* networkInput = input.FindComponentInput(); + networkInput->m_childInputs.clear(); + networkInput->m_childInputs.reserve(entities.size()); + + for (AZ::Entity* child : entities) + { + if(child == component.GetEntity()) + { + continue; // Avoid infinite recursion + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netComp = childEntityHandle.GetNetBindComponent(); + + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // Validate we still have a controller and we aren't in the middle of removing them + if (netComp->HasController()) + { + NetworkInputChild subInput; + subInput.Attach(childEntityHandle); + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + + netComp->CreateInput(subInput.GetNetworkInput(), deltaTime); + + // make sure our input sub commands have the same time as the original + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + networkInput->m_childInputs.emplace_back(subInput); + } + } + } + + void NetworkHierarchyRootComponentController::ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) + { + if (auto* networkInput = input.FindComponentInput()) + { + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + // Build a set of Net IDs for the children + AZStd::unordered_set currentChildren; + NetworkHierarchyRootComponent& component = GetParent(); + for (AZ::Entity* child : component.m_hierarchicalEntities) + { + if (child == component.GetEntity()) // Skip the root entity + { + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + currentChildren.insert(childNetEntitydId); + } + + // Process the input for the child entities + for (NetworkInputChild& subInput : networkInput->m_childInputs) + { + const ConstNetworkEntityHandle& inputOwnerHandle = subInput.GetOwner(); + NetEntityId inputOwnerNetEntitydId = inputOwnerHandle.GetNetEntityId(); + + if (currentChildren.count(inputOwnerNetEntitydId) == 0) + { + // Skip the input for entities which are not a part of this hierarchy + continue; + } + + ConstNetworkEntityHandle localEntityHandle = networkEntityManager->GetEntity(inputOwnerNetEntitydId); + if (localEntityHandle.Exists()) + { + auto* netComp = localEntityHandle.GetNetBindComponent(); + AZ_Assert(netComp, "No NetBindComponent, this should be impossible"); + // We do not rewind entity role changes, so make sure we are the correct role prior to processing + if (netComp->HasController()) + { + subInput.GetNetworkInput().SetClientInputId(input.GetClientInputId()); + netComp->ProcessInput(subInput.GetNetworkInput(), deltaTime); + } + } + } + } + } + + bool NetworkHierarchyRootComponent::SerializeEntityCorrection(AzNetworking::ISerializer& serializer) + { + bool result = true; + + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (AZ::Entity* child : m_hierarchicalEntities) + { + if (child == GetEntity()) + { + // Skip the root entity + continue; + } + + NetEntityId childNetEntitydId = networkEntityManager->GetNetEntityIdById(child->GetId()); + AZ_Assert(childNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle childEntityHandle = networkEntityManager->GetEntity(childNetEntitydId); + NetBindComponent* netBindComponent = childEntityHandle.GetNetBindComponent(); + AZ_Assert(netBindComponent, "No NetBindComponent, this should be impossible"); + + result = result && netBindComponent->SerializeEntityCorrection(serializer); + } + + return result; + } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index e8710cdd6c..42a7fe43e5 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -685,12 +685,21 @@ namespace Multiplayer if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (controlledEntity.Exists()) + INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); + for (auto& netEntity : entityList) { - controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + if (netEntity.Exists()) + { + netEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + } + netEntity.Activate(); + } + + NetworkEntityHandle controlledEntity; + if (entityList.size() > 0) + { + controlledEntity = entityList[0]; } - controlledEntity.Activate(); connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); @@ -800,12 +809,16 @@ namespace Multiplayer // Spawn the default player for this host since the host is also a player (not a dedicated server) if (m_agentType == MultiplayerAgentType::ClientServer) { - NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); - if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) + INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); + + for (NetworkEntityHandle controlledEntity : entityList) { - controlledEntityNetBindComponent->SetAllowAutonomy(true); + if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) + { + controlledEntityNetBindComponent->SetAllowAutonomy(true); + } + controlledEntity.Activate(); } - controlledEntity.Activate(); } AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); @@ -1053,17 +1066,12 @@ namespace Multiplayer } } - NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + INetworkEntityManager::EntityList MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() { PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str())); INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate); - NetworkEntityHandle controlledEntity; - if (!entityList.empty()) - { - controlledEntity = entityList[0]; - } - return controlledEntity; + return entityList; } void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 568d5f7801..9ab78e0f0b 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -146,7 +146,7 @@ namespace Multiplayer void TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds); void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - NetworkEntityHandle SpawnDefaultPlayerPrefab(); + INetworkEntityManager::EntityList SpawnDefaultPlayerPrefab(); AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp index ef338840f9..7794d9026b 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp @@ -32,8 +32,8 @@ namespace Multiplayer if (entity) { - AZ_Assert(networkEntityTracker, "NetworkEntityTracker is not valid"); - m_netBindComponent = networkEntityTracker->GetNetBindComponent(entity); + AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); + m_netBindComponent = m_networkEntityTracker->GetNetBindComponent(entity); if (m_netBindComponent != nullptr) { m_netEntityId = m_netBindComponent->GetNetEntityId(); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index db7f7243cc..b72ea21719 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include namespace Multiplayer @@ -272,15 +274,32 @@ namespace Multiplayer bool safeToExit = true; NetworkEntityHandle entityHandle = m_networkEntityTracker.Get(entityId); - // We also need special handling for the EntityHierarchyComponent as well, since related entities need to be migrated together - //auto* hierarchyController = FindController(nonConstExitingEntityPtr); - //if (hierarchyController) - //{ - // if (hierarchyController->GetParentRelatedEntity()) - // { - // safeToExit = false; - // } - //} + // We also need special handling for the NetworkHierarchy as well, since related entities need to be migrated together + NetworkHierarchyRootComponentController* hierarchyRootController = entityHandle.FindController(); + NetworkHierarchyChildComponentController* hierarchyChildController = entityHandle.FindController(); + + // Find the root entity + AZ::Entity* hierarchyRootEntity = nullptr; + if (hierarchyRootController) + { + hierarchyRootEntity = hierarchyRootController->GetParent().GetHierarchicalRoot(); + } + else if (hierarchyChildController) + { + hierarchyRootEntity = hierarchyChildController->GetParent().GetHierarchicalRoot(); + } + + if (hierarchyRootEntity) + { + NetEntityId rootNetId = GetNetEntityIdById(hierarchyRootEntity->GetId()); + ConstNetworkEntityHandle rootEntityHandle = GetEntity(rootNetId); + + // Check if the root entity is still tracked by this authority + if (rootEntityHandle.Exists() && rootEntityHandle.GetNetBindComponent()->HasController()) + { + safeToExit = false; + } + } // Validate that we aren't already planning to remove this entity if (safeToExit) diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h index 98778cce17..417d9d65f7 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h @@ -12,10 +12,7 @@ namespace Multiplayer { - //! Max number of entities that can be children of our netbound player entity. - static constexpr uint32_t MaxEntityHierarchyChildren = 16; - - //! Used by the EntityHierarchyComponent. This component allows the gameplay programmer to specify inputs for dependent entities. + //! Used by the NetworkHierarchyRootComponent. This component allows the gameplay programmer to specify inputs for dependent entities. //! Since it is possible to for the Client/Server to disagree about the state of related entities, //! this network input encodes the entity that is associated with it. class NetworkInputChild @@ -37,4 +34,6 @@ namespace Multiplayer ConstNetworkEntityHandle m_owner; NetworkInput m_networkInput; }; + + using NetworkInputChildList = AZStd::vector; } diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index e0990aa785..27e59c0bf4 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -110,7 +110,7 @@ namespace Multiplayer auto serializer = [](AZStd::vector& output, const ProcessedObjectStore& object) -> bool { AZ::IO::ByteContainerStream stream(&output); auto& asset = object.GetAsset(); - return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_JSON, &asset, asset.GetType()); + return AZ::Utils::SaveObjectToStream(stream, AZ::DataStream::ST_BINARY, &asset, asset.GetType()); }; auto&& [object, networkSpawnable] = diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp index 9d9cc74d2b..856fd59f9d 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -174,11 +175,11 @@ namespace Multiplayer // Note: Do not add any Client entities after this point, otherwise you stomp over the Autonomous mode m_replicationSet[m_controlledEntity] = { NetEntityRole::Autonomous, 1.0f }; // Always replicate autonomous entities - //auto hierarchyController = FindController(m_ControlledEntity); - //if (hierarchyController != nullptr) - //{ - // CollectControlledEntitiesRecursive(m_replicationSet, *hierarchyController); - //} + auto* hierarchyComponent = m_controlledEntity.FindComponent(); + if (hierarchyComponent != nullptr) + { + UpdateHierarchyReplicationSet(m_replicationSet, *hierarchyComponent); + } } AzNetworking::PacketId ServerToClientReplicationWindow::SendEntityUpdateMessages(NetworkEntityUpdateVector& entityUpdateVector) @@ -326,18 +327,20 @@ namespace Multiplayer } } - //void ServerToClientReplicationWindow::CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController) - //{ - // auto controlledEnts = hierarchyController.GetChildrenRelatedEntities(); - // for (auto& controlledEnt : controlledEnts) - // { - // AZ_Assert(controlledEnt != nullptr, "We have lost a controlled entity unexpectedly"); - // replicationSet[controlledEnt.GetConstEntity()] = EntityReplicationData(EntityNetworkRoleT::e_Autonomous, EntityPrioritySystem::k_MaxPriority); // Always replicate controlled entities - // auto hierarchyController = controlledEnt.FindController(); - // if (hierarchyController != nullptr) - // { - // CollectControlledEntitiesRecursive(replicationSet, *hierarchyController); - // } - // } - //} + void ServerToClientReplicationWindow::UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent) + { + INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); + AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); + + for (const AZ::Entity* controlledEntity : hierarchyComponent.m_hierarchicalEntities) + { + NetEntityId controlledNetEntitydId = networkEntityManager->GetNetEntityIdById(controlledEntity->GetId()); + AZ_Assert(controlledNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); + + ConstNetworkEntityHandle controlledEntityHandle = networkEntityManager->GetEntity(controlledNetEntitydId); + AZ_Assert(controlledEntityHandle != nullptr, "We have lost a controlled entity unexpectedly"); + + replicationSet[controlledEntityHandle] = { NetEntityRole::Autonomous, 1.0f }; + } + } } diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h index b034bde90c..8816209d7b 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h @@ -20,6 +20,7 @@ namespace Multiplayer { class NetSystemComponent; + class NetworkHierarchyRootComponent; class ServerToClientReplicationWindow : public IReplicationWindow @@ -56,7 +57,7 @@ namespace Multiplayer void OnEntityActivated(AZ::Entity* entity); void OnEntityDeactivated(AZ::Entity* entity); - //void CollectControlledEntitiesRecursive(ReplicationSet& replicationSet, EntityHierarchyComponent::Authority& hierarchyController); + void UpdateHierarchyReplicationSet(ReplicationSet& replicationSet, NetworkHierarchyRootComponent& hierarchyComponent); void EvaluateConnection(); void AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, float distanceSquared); diff --git a/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml new file mode 100644 index 0000000000..6b18e5ac70 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index d6dd068c3f..badee73e04 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include namespace Multiplayer { @@ -175,10 +177,10 @@ namespace Multiplayer void CreateSimpleHierarchy(EntityInfo& root, EntityInfo& child) { PopulateHierarchicalEntity(root); - SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Client); + SetupEntity(root.m_entity, root.m_netId, NetEntityRole::Autonomous); PopulateHierarchicalEntity(child); - SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Client); + SetupEntity(child.m_entity, child.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(child.m_entity, root.m_netId); @@ -330,7 +332,7 @@ namespace Multiplayer void CreateDeepHierarchyOnClient(EntityInfo& childOfChild) { PopulateHierarchicalEntity(childOfChild); - SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Client); + SetupEntity(childOfChild.m_entity, childOfChild.m_netId, NetEntityRole::Autonomous); // we need a parent-id value to be present in NetworkTransformComponent (which is in client mode and doesn't have a controller) SetParentIdOnNetworkTransform(childOfChild.m_entity, m_childOfChild->m_netId); @@ -387,4 +389,66 @@ namespace Multiplayer ); } } + + TEST_F(ClientDeepHierarchyTests, CreateProcessInputTest) + { + using MultiplayerTest::TestMultiplayerComponent; + using MultiplayerTest::TestMultiplayerComponentController; + using MultiplayerTest::TestMultiplayerComponentNetworkInput; + + auto* rootNetBind = m_root->m_entity->FindComponent(); + + NetworkInputArray inputArray(rootNetBind->GetEntityHandle()); + NetworkInput& input = inputArray[0]; + + const float deltaTime = 0.16f; + rootNetBind->CreateInput(input, deltaTime); + + auto ValidateCreatedInput = [](const NetworkInput& input, const HierarchyTests::EntityInfo& entityInfo) + { + // Validate test input for the root entity's TestMultiplayerComponent + auto* testInput = input.FindComponentInput(); + EXPECT_NE(testInput, nullptr); + + auto* testMultiplayerComponent = entityInfo.m_entity->FindComponent(); + EXPECT_NE(testMultiplayerComponent, nullptr); + + EXPECT_EQ(testInput->m_ownerId, testMultiplayerComponent->GetId()); + }; + + // Validate root input + ValidateCreatedInput(input, *m_root); + + // Validate children input + { + NetworkHierarchyRootComponentNetworkInput* rootHierarchyInput = input.FindComponentInput(); + const AZStd::vector& childInputs = rootHierarchyInput->m_childInputs; + EXPECT_EQ(childInputs.size(), 2); + ValidateCreatedInput(childInputs[0].GetNetworkInput(), *m_child); + ValidateCreatedInput(childInputs[1].GetNetworkInput(), *m_childOfChild); + } + + // Test ProcessInput + { + AZStd::unordered_set inputProcessedEntities; + size_t processInputCallCounter = 0; + auto processInputCallback = [&inputProcessedEntities, &processInputCallCounter](NetEntityId netEntityId) + { + inputProcessedEntities.insert(netEntityId); + processInputCallCounter++; + }; + + // Set the callbacks for processing input. This allows us to inspect how many times the input was processed + // and which entity's controller was invoked. + m_root->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_child->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + m_childOfChild->m_entity->FindComponent()->m_processInputCallback = processInputCallback; + + rootNetBind->ProcessInput(input, deltaTime); + + EXPECT_EQ(processInputCallCounter, 3); + EXPECT_EQ(inputProcessedEntities, + AZStd::unordered_set({ m_root->m_netId, m_child->m_netId, m_childOfChild->m_netId })); + } + } } diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index d6f65918c0..38027c11a9 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -30,6 +30,7 @@ #include #include #include +#include namespace Multiplayer { @@ -93,6 +94,12 @@ namespace Multiplayer m_netTransformDescriptor.reset(NetworkTransformComponent::CreateDescriptor()); m_netTransformDescriptor->Reflect(m_serializeContext.get()); + m_testMultiplayerComponentDescriptor.reset(MultiplayerTest::TestMultiplayerComponent::CreateDescriptor()); + m_testMultiplayerComponentDescriptor->Reflect(m_serializeContext.get()); + + m_testInputDriverComponentDescriptor.reset(MultiplayerTest::TestInputDriverComponent::CreateDescriptor()); + m_testInputDriverComponentDescriptor->Reflect(m_serializeContext.get()); + m_mockMultiplayer = AZStd::make_unique>(); AZ::Interface::Register(m_mockMultiplayer.get()); @@ -103,6 +110,7 @@ namespace Multiplayer GetMultiplayer()->GetStats().ReserveComponentStats(Multiplayer::InvalidNetComponentId, 50, 0); m_mockNetworkEntityManager = AZStd::make_unique>(); + AZ::Interface::Register(m_mockNetworkEntityManager.get()); ON_CALL(*m_mockNetworkEntityManager, AddEntityToEntityMap(_, _)).WillByDefault(Invoke(this, &HierarchyTests::AddEntityToEntityMap)); ON_CALL(*m_mockNetworkEntityManager, GetEntity(_)).WillByDefault(Invoke(this, &HierarchyTests::GetEntity)); @@ -136,6 +144,7 @@ namespace Multiplayer m_multiplayerComponentRegistry = AZStd::make_unique(); ON_CALL(*m_mockNetworkEntityManager, GetMultiplayerComponentRegistry()).WillByDefault(Return(m_multiplayerComponentRegistry.get())); RegisterMultiplayerComponents(); + MultiplayerTest::RegisterMultiplayerComponents(); } void TearDown() override @@ -157,6 +166,7 @@ namespace Multiplayer AZ::Interface::Unregister(m_mockNetworkTime.get()); AZ::Interface::Unregister(m_mockTime.get()); + AZ::Interface::Unregister(m_mockNetworkEntityManager.get()); AZ::Interface::Unregister(m_mockMultiplayer.get()); AZ::Interface::Unregister(m_mockComponentApplicationRequests.get()); @@ -165,6 +175,8 @@ namespace Multiplayer m_mockNetworkEntityManager.reset(); m_mockMultiplayer.reset(); + m_testInputDriverComponentDescriptor.reset(); + m_testMultiplayerComponentDescriptor.reset(); m_transformDescriptor.reset(); m_netTransformDescriptor.reset(); m_hierarchyRootDescriptor.reset(); @@ -186,6 +198,8 @@ namespace Multiplayer AZStd::unique_ptr m_hierarchyRootDescriptor; AZStd::unique_ptr m_hierarchyChildDescriptor; AZStd::unique_ptr m_netTransformDescriptor; + AZStd::unique_ptr m_testMultiplayerComponentDescriptor; + AZStd::unique_ptr m_testInputDriverComponentDescriptor; AZStd::unique_ptr> m_mockMultiplayer; AZStd::unique_ptr m_mockNetworkEntityManager; @@ -394,6 +408,9 @@ namespace Multiplayer entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + switch (entityInfo.m_role) { case EntityInfo::Role::Root: diff --git a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp new file mode 100644 index 0000000000..64381bf195 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp @@ -0,0 +1,205 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + using namespace testing; + using namespace ::UnitTest; + + class NetworkInputTests : public HierarchyTests + { + public: + void SetUp() override + { + HierarchyTests::SetUp(); + + m_root = AZStd::make_unique(1, "root", NetEntityId{ 1 }, EntityInfo::Role::Root); + + PopulateNetworkEntity(*m_root); + SetupEntity(m_root->m_entity, m_root->m_netId, NetEntityRole::Authority); + + // Create an entity replicator for the root entity + const NetworkEntityHandle rootHandle(m_root->m_entity.get(), m_networkEntityTracker.get()); + m_root->m_replicator = AZStd::make_unique(*m_entityReplicationManager, m_mockConnection.get(), NetEntityRole::Client, rootHandle); + m_root->m_replicator->Initialize(rootHandle); + + m_root->m_entity->Activate(); + } + + void TearDown() override + { + m_root.reset(); + + HierarchyTests::TearDown(); + } + + void PopulateNetworkEntity(const EntityInfo& entityInfo) + { + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + entityInfo.m_entity->CreateComponent(); + } + + AZStd::unique_ptr m_root; + }; + + constexpr float BLEND_FACTOR_SCALE = 1.1f; + constexpr uint32_t TIME_SCALE = 10; + + TEST_F(NetworkInputTests, NetworkInputMembers) + { + const NetworkEntityHandle handle(m_root->m_entity.get(), m_networkEntityTracker.get()); + NetworkInputArray inArray = NetworkInputArray(handle); + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + inArray[i].SetClientInputId(ClientInputId(i)); + inArray[i].SetHostFrameId(HostFrameId(i)); + inArray[i].SetHostBlendFactor(i * BLEND_FACTOR_SCALE); + inArray[i].SetHostTimeMs(AZ::TimeMs(i * TIME_SCALE)); + + EXPECT_EQ(inArray[i].GetClientInputId(), ClientInputId(i)); + EXPECT_EQ(inArray[i].GetHostFrameId(), HostFrameId(i)); + EXPECT_NEAR(inArray[i].GetHostBlendFactor(), i * BLEND_FACTOR_SCALE, 0.001f); + EXPECT_EQ(inArray[i].GetHostTimeMs(), AZ::TimeMs(i * TIME_SCALE)); + } + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + ClientInputId& cid = inArray[i].ModifyClientInputId(); + cid = ClientInputId(i * 2); + HostFrameId& hid = inArray[i].ModifyHostFrameId(); + hid = HostFrameId(i * 2); + AZ::TimeMs& time = inArray[i].ModifyHostTimeMs(); + time = AZ::TimeMs(i * 2 * TIME_SCALE); + + EXPECT_EQ(inArray[i].GetClientInputId(), cid); + EXPECT_EQ(inArray[i].GetHostFrameId(), hid); + EXPECT_EQ(inArray[i].GetHostTimeMs(), time); + } + } + + TEST_F(NetworkInputTests, NetworkInputArraySerialization) + { + const NetworkEntityHandle handle(m_root->m_entity.get(), m_networkEntityTracker.get()); + NetworkInputArray inArray = NetworkInputArray(handle); + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + inArray[i].SetClientInputId(ClientInputId(i)); + inArray[i].SetHostFrameId(HostFrameId(i)); + inArray[i].SetHostBlendFactor(i * BLEND_FACTOR_SCALE); + inArray[i].SetHostTimeMs(AZ::TimeMs(i * TIME_SCALE)); + } + + AZStd::array buffer; + AzNetworking::NetworkInputSerializer inSerializer(buffer.data(), static_cast(buffer.size())); + + // Always serialize the full first element + EXPECT_TRUE(inArray.Serialize(inSerializer)); + + NetworkInputArray outArray; + AzNetworking::NetworkOutputSerializer outSerializer(buffer.data(), static_cast(buffer.size())); + + EXPECT_TRUE(outArray.Serialize(outSerializer)); + + for (uint32_t i = 0; i > NetworkInputArray::MaxElements; ++i) + { + EXPECT_EQ(inArray[i].GetClientInputId(), outArray[i].GetClientInputId()); + EXPECT_EQ(inArray[i].GetHostFrameId(), outArray[i].GetHostFrameId()); + EXPECT_NEAR(inArray[i].GetHostBlendFactor(), outArray[i].GetHostBlendFactor(),0.001f); + EXPECT_EQ(inArray[i].GetHostTimeMs(), outArray[i].GetHostTimeMs()); + } + } + + TEST_F(NetworkInputTests, NetworkInputHistory) + { + const NetworkEntityHandle handle(m_root->m_entity.get(), m_networkEntityTracker.get()); + NetworkInputArray inArray = NetworkInputArray(handle); + NetworkInputHistory inHistory = NetworkInputHistory(); + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + inArray[i].SetClientInputId(ClientInputId(i)); + inArray[i].SetHostFrameId(HostFrameId(i)); + inArray[i].SetHostBlendFactor(i * BLEND_FACTOR_SCALE); + inArray[i].SetHostTimeMs(AZ::TimeMs(i * TIME_SCALE)); + + inHistory.PushBack(inArray[i]); + } + + EXPECT_EQ(inHistory.Size(), NetworkInputArray::MaxElements); + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + NetworkInput input = inHistory.Front(); + EXPECT_EQ(input.GetClientInputId(), ClientInputId(i)); + EXPECT_EQ(input.GetHostFrameId(), HostFrameId(i)); + EXPECT_NEAR(input.GetHostBlendFactor(), i * BLEND_FACTOR_SCALE, 0.001f); + EXPECT_EQ(input.GetHostTimeMs(), AZ::TimeMs(i * TIME_SCALE)); + inHistory.PopFront(); + } + + EXPECT_EQ(inHistory.Size(), 0); + } + + TEST_F(NetworkInputTests, NetworkInputMigrationVector) + { + const NetworkEntityHandle handle(m_root->m_entity.get(), m_networkEntityTracker.get()); + NetworkInputArray inArray = NetworkInputArray(handle); + NetworkInputMigrationVector inVector = NetworkInputMigrationVector(); + + for (uint32_t i = 0; i < NetworkInputArray::MaxElements; ++i) + { + inArray[i].SetClientInputId(ClientInputId(i)); + inArray[i].SetHostFrameId(HostFrameId(i)); + inArray[i].SetHostBlendFactor(i * BLEND_FACTOR_SCALE); + inArray[i].SetHostTimeMs(AZ::TimeMs(i * TIME_SCALE)); + + inVector.PushBack(inArray[i]); + } + + EXPECT_EQ(inVector.GetSize(), NetworkInputArray::MaxElements); + + AZStd::array buffer; + AzNetworking::NetworkInputSerializer inSerializer(buffer.data(), static_cast(buffer.size())); + + // Always serialize the full first element + EXPECT_TRUE(inVector.Serialize(inSerializer)); + + NetworkInputArray outArray; + AzNetworking::NetworkOutputSerializer outSerializer(buffer.data(), static_cast(buffer.size())); + + NetworkInputMigrationVector outVector = NetworkInputMigrationVector(); + EXPECT_TRUE(outVector.Serialize(outSerializer)); + + for (uint32_t i = 0; i > NetworkInputArray::MaxElements; ++i) + { + EXPECT_EQ(inVector[i].GetClientInputId(), outVector[i].GetClientInputId()); + EXPECT_EQ(inVector[i].GetHostFrameId(), outVector[i].GetHostFrameId()); + EXPECT_NEAR(inVector[i].GetHostBlendFactor(), outVector[i].GetHostBlendFactor(),0.001f); + EXPECT_EQ(inVector[i].GetHostTimeMs(), outVector[i].GetHostTimeMs()); + } + EXPECT_EQ(inVector.GetSize(), outVector.GetSize()); + } +} diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp new file mode 100644 index 0000000000..ac723300a9 --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.cpp @@ -0,0 +1,76 @@ +/* +* 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 + +namespace MultiplayerTest +{ + void TestInputDriverComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + } + + void TestMultiplayerComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + TestMultiplayerComponentBase::Reflect(context); + } + + void TestMultiplayerComponent::OnInit() + { + } + + void TestMultiplayerComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + TestMultiplayerComponentController::TestMultiplayerComponentController(TestMultiplayerComponent& parent) + : TestMultiplayerComponentControllerBase(parent) + { + } + + void TestMultiplayerComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + } + + void TestMultiplayerComponentController::CreateInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto* networkInput = input.FindComponentInput(); + networkInput->m_ownerId = GetParent().GetId(); + } + + void TestMultiplayerComponentController::ProcessInput(Multiplayer::NetworkInput& input, [[maybe_unused]] float deltaTime) + { + auto& component = GetParent(); + [[maybe_unused]] auto* networkInput = input.FindComponentInput(); + AZ_Assert(networkInput->m_ownerId == component.GetId(), "Input Id doesn't match the owner component Id"); + + if (component.m_processInputCallback) + { + component.m_processInputCallback(GetNetEntityId()); + } + } +} diff --git a/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h new file mode 100644 index 0000000000..4cf0be2c2d --- /dev/null +++ b/Gems/Multiplayer/Code/Tests/TestMultiplayerComponent.h @@ -0,0 +1,61 @@ +/* +* 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 MultiplayerTest +{ + // Dummy class for satisfying "MultiplayerInputDriver" component dependency + class TestInputDriverComponent : public AZ::Component + { + public: + AZ_COMPONENT(TestInputDriverComponent, "{C3877905-3B61-45AE-A636-9845C3AAA39D}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.emplace_back(AZ_CRC_CE("MultiplayerInputDriver")); + } + + void Activate() override {} + void Deactivate() override {} + }; + + // Test multiplayer component with ability to create and process network input + class TestMultiplayerComponent + : public TestMultiplayerComponentBase + { + public: + AZ_MULTIPLAYER_COMPONENT(MultiplayerTest::TestMultiplayerComponent, s_testMultiplayerComponentConcreteUuid, MultiplayerTest::TestMultiplayerComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + AZStd::function m_processInputCallback; + }; + + // Multiplayer controller for the test component + class TestMultiplayerComponentController + : public TestMultiplayerComponentControllerBase + { + public: + TestMultiplayerComponentController(TestMultiplayerComponent& parent); + + //! TestMultiplayerComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! MultiplayerController interface + void CreateInput(Multiplayer::NetworkInput& input, float deltaTime) override; + void ProcessInput(Multiplayer::NetworkInput& input, float deltaTime) override; + }; +} diff --git a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake index 2114143b68..adc391fe31 100644 --- a/Gems/Multiplayer/Code/multiplayer_tests_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tests_files.cmake @@ -7,6 +7,12 @@ # set(FILES + Include/Multiplayer/AutoGen/AutoComponentTypes_Header.jinja + Include/Multiplayer/AutoGen/AutoComponentTypes_Source.jinja + Include/Multiplayer/AutoGen/AutoComponent_Common.jinja + Include/Multiplayer/AutoGen/AutoComponent_Header.jinja + Include/Multiplayer/AutoGen/AutoComponent_Source.jinja + Tests/AutoGen/TestMultiplayerComponent.AutoComponent.xml Tests/ClientHierarchyTests.cpp Tests/ServerHierarchyBenchmarks.cpp Tests/CommonHierarchySetup.h @@ -15,8 +21,11 @@ set(FILES Tests/Main.cpp Tests/MockInterfaces.h Tests/MultiplayerSystemTests.cpp + Tests/NetworkInputTests.cpp Tests/NetworkTransformTests.cpp Tests/RewindableContainerTests.cpp Tests/RewindableObjectTests.cpp Tests/ServerHierarchyTests.cpp + Tests/TestMultiplayerComponent.h + Tests/TestMultiplayerComponent.cpp ) diff --git a/Gems/Profiler/CMakeLists.txt b/Gems/Profiler/CMakeLists.txt new file mode 100644 index 0000000000..2bb380fae3 --- /dev/null +++ b/Gems/Profiler/CMakeLists.txt @@ -0,0 +1,9 @@ +# +# 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 +# +# + +add_subdirectory(Code) diff --git a/Gems/Profiler/Code/CMakeLists.txt b/Gems/Profiler/Code/CMakeLists.txt new file mode 100644 index 0000000000..6e4adca67c --- /dev/null +++ b/Gems/Profiler/Code/CMakeLists.txt @@ -0,0 +1,64 @@ +# +# 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 +# +# + +# data portion +ly_add_target( + NAME Profiler.Static STATIC + NAMESPACE Gem + FILES_CMAKE + profiler_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework +) + +ly_add_target( + NAME Profiler ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + profiler_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::Profiler.Static +) + +ly_create_alias(NAME Profiler.Servers NAMESPACE Gem TARGETS Gem::Profiler) +ly_create_alias(NAME Profiler.Builders NAMESPACE Gem TARGETS Gem::Profiler) + +# visualization portion +ly_add_target( + NAME ProfilerImGui ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + profiler_imgui_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + Gem::Profiler.Static + Gem::ImGui.imguilib + RUNTIME_DEPENDENCIES + Gem::ImGui.imguilib +) + +ly_create_alias(NAME Profiler.Clients NAMESPACE Gem TARGETS Gem::ProfilerImGui) +ly_create_alias(NAME Profiler.Tools NAMESPACE Gem TARGETS Gem::ProfilerImGui) diff --git a/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h new file mode 100644 index 0000000000..22352185d3 --- /dev/null +++ b/Gems/Profiler/Code/Include/Profiler/ProfilerBus.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include +#include + +namespace Profiler +{ + class ProfilerRequests + { + public: + AZ_RTTI(ProfilerRequests, "{3757c4e5-1941-457c-85ae-16305e17a4c6}"); + virtual ~ProfilerRequests() = default; + + //! Enable/Disable the CpuProfiler + virtual void SetProfilerEnabled(bool enabled) = 0; + + //! Dump a single frame of Cpu profiling data + virtual bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) = 0; + + //! Start a multiframe capture of CPU profiling data. + virtual bool BeginContinuousCpuProfilingCapture() = 0; + + //! End and dump an in-progress continuous capture. + virtual bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) = 0; + }; + + class ProfilerBusTraits + : public AZ::EBusTraits + { + public: + // EBusTraits overrides + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + }; + + class ProfilerNotifications + : public AZ::EBusTraits + { + public: + virtual ~ProfilerNotifications() = default; + + //! Notify when the current CpuProfilingStatistics capture is finished + //! @param result Set to true if it's finished successfully + //! @param info The output file path or error information which depends on the return. + virtual void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) = 0; + }; + + using ProfilerInterface = AZ::Interface; + using ProfilerRequestBus = AZ::EBus; + using ProfilerNotificationBus = AZ::EBus; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h b/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h new file mode 100644 index 0000000000..b6a69ae7b5 --- /dev/null +++ b/Gems/Profiler/Code/Include/Profiler/ProfilerImGuiBus.h @@ -0,0 +1,27 @@ +/* + * 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 Profiler +{ + class ProfilerImGuiRequests + { + public: + AZ_RTTI(ProfilerImGuiRequests, "{E0443400-D108-4D3F-8FF5-4F076FCF6D13}"); + virtual ~ProfilerImGuiRequests() = default; + + // special request to render the CPU profiler window in a non-standard way + // e.g not through ImGuiUpdateListenerBus::OnImGuiUpdate + virtual void ShowCpuProfilerWindow(bool& keepDrawing) = 0; + }; + + using ProfilerImGuiInterface = AZ::Interface; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfiler.h b/Gems/Profiler/Code/Source/CpuProfiler.h new file mode 100644 index 0000000000..23130efa63 --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfiler.h @@ -0,0 +1,83 @@ +/* + * 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 + +namespace Profiler +{ + //! Structure that is used to cache a timed region into the thread's local storage. + struct CachedTimeRegion + { + //! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region + //! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown. + struct GroupRegionName + { + GroupRegionName() = delete; + GroupRegionName(const char* const group, const char* const region); + + const char* m_groupName = nullptr; + const char* m_regionName = nullptr; + + struct Hash + { + AZStd::size_t operator()(const GroupRegionName& name) const; + }; + bool operator==(const GroupRegionName& other) const; + }; + + CachedTimeRegion() = default; + explicit CachedTimeRegion(const GroupRegionName& groupRegionName); + CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); + + GroupRegionName m_groupRegionName{nullptr, nullptr}; + + uint16_t m_stackDepth = 0u; + AZStd::sys_time_t m_startTick = 0; + AZStd::sys_time_t m_endTick = 0; + }; + + //! Interface class of the CpuProfiler + class CpuProfiler + { + public: + using ThreadTimeRegionMap = AZStd::unordered_map>; + using TimeRegionMap = AZStd::unordered_map; + + AZ_RTTI(CpuProfiler, "{127C1D0B-BE05-4E18-A8F6-24F3EED2ECA6}"); + + CpuProfiler() = default; + virtual ~CpuProfiler() = default; + + AZ_DISABLE_COPY_MOVE(CpuProfiler); + + static CpuProfiler* Get(); + + //! Get the last frame's TimeRegionMap + virtual const TimeRegionMap& GetTimeRegionMap() const = 0; + + //! Begin a continuous capture. Blocks the profiler from being toggled off until EndContinuousCapture is called. + [[nodiscard]] virtual bool BeginContinuousCapture() = 0; + + //! Flush the CPU Profiler's saved data into the passed ring buffer . + [[nodiscard]] virtual bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) = 0; + + virtual bool IsContinuousCaptureInProgress() const = 0; + + //! Enable/Disable the CpuProfiler + virtual void SetProfilerEnabled(bool enabled) = 0; + + virtual bool IsProfilerEnabled() const = 0 ; + }; +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp new file mode 100644 index 0000000000..c88afdecd0 --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.cpp @@ -0,0 +1,437 @@ +/* + * 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 Profiler +{ + thread_local CpuTimingLocalStorage* CpuProfilerImpl::ms_threadLocalStorage = nullptr; + + // --- CpuProfiler --- + + CpuProfiler* CpuProfiler::Get() + { + return AZ::Interface::Get(); + } + + // --- CachedTimeRegion --- + + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName) + { + m_groupRegionName = groupRegionName; + } + + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) + { + m_groupRegionName = groupRegionName; + m_stackDepth = stackDepth; + m_startTick = startTick; + m_endTick = endTick; + } + + // --- GroupRegionName --- + + CachedTimeRegion::GroupRegionName::GroupRegionName(const char* const group, const char* const region) : + m_groupName(group), + m_regionName(region) + { + } + + AZStd::size_t CachedTimeRegion::GroupRegionName::Hash::operator()(const CachedTimeRegion::GroupRegionName& name) const + { + AZStd::size_t seed = 0; + AZStd::hash_combine(seed, name.m_groupName); + AZStd::hash_combine(seed, name.m_regionName); + return seed; + } + + bool CachedTimeRegion::GroupRegionName::operator==(const GroupRegionName& other) const + { + return (m_groupName == other.m_groupName) && (m_regionName == other.m_regionName); + } + + + // --- CpuProfilerImpl --- + + void CpuProfilerImpl::Init() + { + AZ::Interface::Register(this); + AZ::Interface::Register(this); + m_initialized = true; + AZ::SystemTickBus::Handler::BusConnect(); + m_continuousCaptureData.set_capacity(10); + } + + void CpuProfilerImpl::Shutdown() + { + if (!m_initialized) + { + return; + } + // When this call is made, no more thread profiling calls can be performed anymore + AZ::Interface::Unregister(this); + AZ::Interface::Unregister(this); + + // Wait for the remaining threads that might still be processing its profiling calls + AZStd::unique_lock shutdownLock(m_shutdownMutex); + + m_enabled = false; + + // Cleanup all TLS + m_registeredThreads.clear(); + m_timeRegionMap.clear(); + m_initialized = false; + m_continuousCaptureInProgress.store(false); + m_continuousCaptureData.clear(); + AZ::SystemTickBus::Handler::BusDisconnect(); + } + + void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) + { + // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. + if (m_shutdownMutex.try_lock_shared()) + { + if (m_enabled) + { + // Lazy initialization, creates an instance of the Thread local data if it's not created, and registers it + RegisterThreadStorage(); + + // Push it to the stack + CachedTimeRegion timeRegion({budget->Name(), eventName}); + ms_threadLocalStorage->RegionStackPushBack(timeRegion); + } + + m_shutdownMutex.unlock_shared(); + } + } + + void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget) + { + // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. + if (m_shutdownMutex.try_lock_shared()) + { + // guard against enabling mid-marker + if (m_enabled && ms_threadLocalStorage != nullptr) + { + ms_threadLocalStorage->RegionStackPopBack(); + } + + m_shutdownMutex.unlock_shared(); + } + } + + const CpuProfiler::TimeRegionMap& CpuProfilerImpl::GetTimeRegionMap() const + { + return m_timeRegionMap; + } + + bool CpuProfilerImpl::BeginContinuousCapture() + { + bool expected = false; + if (m_continuousCaptureInProgress.compare_exchange_strong(expected, true)) + { + m_enabled = true; + AZ_TracePrintf("Profiler", "Continuous capture started\n"); + return true; + } + + AZ_TracePrintf("Profiler", "Attempting to start a continuous capture while one already in progress"); + return false; + } + + bool CpuProfilerImpl::EndContinuousCapture(AZStd::ring_buffer& flushTarget) + { + if (!m_continuousCaptureInProgress.load()) + { + AZ_TracePrintf("Profiler", "Attempting to end a continuous capture while one not in progress"); + return false; + } + + if (m_continuousCaptureEndingMutex.try_lock()) + { + m_enabled = false; + flushTarget = AZStd::move(m_continuousCaptureData); + m_continuousCaptureData.clear(); + AZ_TracePrintf("Profiler", "Continuous capture ended\n"); + m_continuousCaptureInProgress.store(false); + + m_continuousCaptureEndingMutex.unlock(); + return true; + } + + return false; + } + + bool CpuProfilerImpl::IsContinuousCaptureInProgress() const + { + return m_continuousCaptureInProgress.load(); + } + + void CpuProfilerImpl::SetProfilerEnabled(bool enabled) + { + AZStd::unique_lock lock(m_threadRegisterMutex); + + // Early out if the state is already the same or a continuous capture is in progress + if (m_enabled == enabled || m_continuousCaptureInProgress.load()) + { + return; + } + + // Set the dirty flag in all the TLS to clear the caches + if (enabled) + { + // Iterate through all the threads, and set the clearing flag + for (auto& threadLocal : m_registeredThreads) + { + threadLocal->m_clearContainers = true; + } + + m_enabled = true; + } + else + { + m_enabled = false; + } + } + + bool CpuProfilerImpl::IsProfilerEnabled() const + { + return m_enabled; + } + + void CpuProfilerImpl::OnSystemTick() + { + if (!m_enabled) + { + return; + } + + if (m_continuousCaptureInProgress.load() && m_continuousCaptureEndingMutex.try_lock()) + { + if (m_continuousCaptureData.full() && m_continuousCaptureData.size() != MaxFramesToSave) + { + const AZStd::size_t size = m_continuousCaptureData.size(); + m_continuousCaptureData.set_capacity(AZStd::min(MaxFramesToSave, size + size / 2)); + } + + m_continuousCaptureData.push_back(AZStd::move(m_timeRegionMap)); + m_timeRegionMap.clear(); + m_continuousCaptureEndingMutex.unlock(); + } + + AZStd::unique_lock lock(m_threadRegisterMutex); + + // Iterate through all the threads, and collect the thread's cached time regions + TimeRegionMap newMap; + for (auto& threadLocal : m_registeredThreads) + { + ThreadTimeRegionMap& threadMapEntry = newMap[threadLocal->m_executingThreadId]; + threadLocal->TryFlushCachedMap(threadMapEntry); + } + + // Clear all TLS that flagged themselves to be deleted, meaning that the thread is already terminated + AZStd::remove_if(m_registeredThreads.begin(), m_registeredThreads.end(), [](const AZStd::intrusive_ptr& thread) + { + return thread->m_deleteFlag.load(); + }); + + // Update our saved time regions to the last frame's collected data + m_timeRegionMap = AZStd::move(newMap); + } + + void CpuProfilerImpl::RegisterThreadStorage() + { + AZStd::unique_lock lock(m_threadRegisterMutex); + if (!ms_threadLocalStorage) + { + ms_threadLocalStorage = aznew CpuTimingLocalStorage(); + m_registeredThreads.emplace_back(ms_threadLocalStorage); + } + } + + // --- CpuTimingLocalStorage --- + + CpuTimingLocalStorage::CpuTimingLocalStorage() + { + m_executingThreadId = AZStd::this_thread::get_id(); + } + + CpuTimingLocalStorage::~CpuTimingLocalStorage() + { + m_deleteFlag = true; + } + + void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) + { + // If it was (re)enabled, clear the lists first + if (m_clearContainers) + { + m_clearContainers = false; + + m_stackLevel = 0; + m_cachedTimeRegionMap.clear(); + m_timeRegionStack.clear(); + m_cachedTimeRegions.clear(); + } + + timeRegion.m_stackDepth = aznumeric_cast(m_stackLevel); + + AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize."); + m_timeRegionStack.push_back(timeRegion); + + // Increment the stack + m_stackLevel++; + + // Set the starting time at the end, to avoid recording the minor overhead + m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks(); + } + + void CpuTimingLocalStorage::RegionStackPopBack() + { + // Early out when the stack is empty, this might happen when the profiler was enabled while the thread encountered profiling markers + if (m_timeRegionStack.empty()) + { + return; + } + + // Get the end timestamp here, to avoid the minor overhead + const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks(); + + AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty."); + CachedTimeRegion back = m_timeRegionStack.back(); + m_timeRegionStack.pop_back(); + + // Set the ending time + back.m_endTick = endRegionTime; + + // Decrement the stack + m_stackLevel--; + + // Add an entry to the cached region + AddCachedRegion(back); + } + + // Gets called when region ends and all data is set + void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached) + { + if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName]) + { + return; + } + // Add an entry to the cached region + m_cachedTimeRegions.push_back(timeRegionCached); + + // If the stack is empty, add it to the local cache map. Only gets called when the stack is empty + // NOTE: this is where the largest overhead will be, but due to it only being called when the stack is empty + // (i.e when the root region ended), this overhead won't affect any time regions. + // The exception being for functions that are being profiled and create/spawn threads that are also profiled. Unfortunately, in this + // case, the overhead of the profiled threads will be added to the main thread. + if (m_timeRegionStack.empty()) + { + AZStd::unique_lock lock(m_cachedTimeRegionMutex); + + // Add the cached regions to the map + for (auto& cachedTimeRegion : m_cachedTimeRegions) + { + const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName; + AZStd::vector& regionVec = m_cachedTimeRegionMap[regionName]; + regionVec.push_back(cachedTimeRegion); + if (regionVec.size() >= TimeRegionStackSize) + { + m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); + } + } + + // Clear the cached regions + m_cachedTimeRegions.clear(); + } + } + + void CpuTimingLocalStorage::TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedTimeRegionMap) + { + // Try to lock, if it's already in use (the cached regions in the array are being copied to the map) + // it'll show up in the next iteration when the user requests it. + if (m_cachedTimeRegionMutex.try_lock()) + { + // Only flush cached time regions if there are entries available + if (!m_cachedTimeRegionMap.empty()) + { + cachedTimeRegionMap = AZStd::move(m_cachedTimeRegionMap); + m_cachedTimeRegionMap.clear(); + m_hitSizeLimitMap.clear(); + } + m_cachedTimeRegionMutex.unlock(); + } + } + + // --- CpuProfilingStatisticsSerializer --- + + CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData) + { + // Create serializable entries + for (const auto& timeRegionMap : continuousData) + { + for (const auto& [threadId, regionMap] : timeRegionMap) + { + for (const auto& [regionName, regionVec] : regionMap) + { + for (const auto& region : regionVec) + { + m_cpuProfilingStatisticsSerializerEntries.emplace_back(region, threadId); + } + } + } + } + } + + void CpuProfilingStatisticsSerializer::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("cpuProfilingStatisticsSerializerEntries", &CpuProfilingStatisticsSerializer::m_cpuProfilingStatisticsSerializerEntries); + } + + CpuProfilingStatisticsSerializerEntry::Reflect(context); + } + + // --- CpuProfilingStatisticsSerializerEntry --- + + CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry( + const CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId) + { + m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName; + m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName; + m_stackDepth = cachedTimeRegion.m_stackDepth; + m_startTick = cachedTimeRegion.m_startTick; + m_endTick = cachedTimeRegion.m_endTick; + m_threadId = AZStd::hash{}(threadId); + } + + void CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("groupName", &CpuProfilingStatisticsSerializerEntry::m_groupName) + ->Field("regionName", &CpuProfilingStatisticsSerializerEntry::m_regionName) + ->Field("stackDepth", &CpuProfilingStatisticsSerializerEntry::m_stackDepth) + ->Field("startTick", &CpuProfilingStatisticsSerializerEntry::m_startTick) + ->Field("endTick", &CpuProfilingStatisticsSerializerEntry::m_endTick) + ->Field("threadId", &CpuProfilingStatisticsSerializerEntry::m_threadId); + } + } +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/CpuProfilerImpl.h b/Gems/Profiler/Code/Source/CpuProfilerImpl.h new file mode 100644 index 0000000000..1046b72cff --- /dev/null +++ b/Gems/Profiler/Code/Source/CpuProfilerImpl.h @@ -0,0 +1,183 @@ +/* + * 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 +#include + +namespace Profiler +{ + //! Thread local class to keep track of the thread's cached time regions. + //! Each thread keeps track of its own time regions, which is communicated from the CpuProfilerImpl. + //! The CpuProfilerImpl is able to request the cached time regions from the CpuTimingLocalStorage. + class CpuTimingLocalStorage + : public AZStd::intrusive_refcount + { + friend class CpuProfilerImpl; + + public: + AZ_CLASS_ALLOCATOR(CpuTimingLocalStorage, AZ::OSAllocator, 0); + + CpuTimingLocalStorage(); + ~CpuTimingLocalStorage(); + + private: + // Maximum stack size + static constexpr uint32_t TimeRegionStackSize = 2048u; + + // Adds a region to the stack, gets called each time a region begins + void RegionStackPushBack(CachedTimeRegion& timeRegion); + + // Pops a region from the stack, gets called each time a region ends + void RegionStackPopBack(); + + // Add a new cached time region. If the stack is empty, flush all entries to the cached map + void AddCachedRegion(const CachedTimeRegion& timeRegionCached); + + // Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked + void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap); + + AZStd::thread_id m_executingThreadId; + // Keeps track of the current thread's stack depth + uint32_t m_stackLevel = 0u; + + // Cached region map, will be flushed to the system's map when the system requests it + CpuProfiler::ThreadTimeRegionMap m_cachedTimeRegionMap; + + // Use fixed vectors to avoid re-allocating new elements + // Keeps track of the regions that added and removed using the macro + AZStd::fixed_vector m_timeRegionStack; + + // Keeps track of regions that completed (i.e regions that was pushed and popped from the stack) + // Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be + // copied to the map. + AZStd::fixed_vector m_cachedTimeRegions; + AZStd::mutex m_cachedTimeRegionMutex; + + // Dirty flag which is set when the CpuProfiler's enabled state is set from false to true + AZStd::atomic_bool m_clearContainers = false; + + // When the thread is terminated, it will flag itself for deletion + AZStd::atomic_bool m_deleteFlag = false; + + // Keep track of the regions that have hit the size limit so we don't have to lock to check + AZStd::map m_hitSizeLimitMap; + }; + + //! CpuProfiler will keep track of the registered threads, and + //! forwards the request to profile a region to the appropriate thread. The user is able to request all + //! cached regions, which are stored on a per thread frequency. + class CpuProfilerImpl final + : public AZ::Debug::Profiler + , public CpuProfiler + , public AZ::SystemTickBus::Handler + { + friend class CpuTimingLocalStorage; + + public: + AZ_TYPE_INFO(CpuProfilerImpl, "{10E9D394-FC83-4B45-B2B8-807C6BF07BF0}"); + AZ_CLASS_ALLOCATOR(CpuProfilerImpl, AZ::OSAllocator, 0); + + CpuProfilerImpl() = default; + ~CpuProfilerImpl() = default; + + //! Registers the CpuProfilerImpl instance to the interface + void Init(); + //! Unregisters the CpuProfilerImpl instance from the interface + void Shutdown(); + + // AZ::SystemTickBus::Handler overrides + // When fired, the profiler collects all profiling data from registered threads and updates + // m_timeRegionMap so that the next frame has up-to-date profiling data. + void OnSystemTick() final override; + + //! AZ::Debug::Profiler overrides... + void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override; + void EndRegion(const AZ::Debug::Budget* budget) final override; + + //! CpuProfiler overrides... + const TimeRegionMap& GetTimeRegionMap() const final override; + bool BeginContinuousCapture() final override; + bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) final override; + bool IsContinuousCaptureInProgress() const final override; + void SetProfilerEnabled(bool enabled) final override; + bool IsProfilerEnabled() const final override; + + private: + static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps + static constexpr AZStd::size_t MaxRegionStringPoolSize = 16384; // Max amount of unique strings to save in the pool before throwing warnings. + + // Lazily create and register the local thread data + void RegisterThreadStorage(); + + // ThreadId -> ThreadTimeRegionMap + // On the start of each frame, this map will be updated with the last frame's profiling data. + TimeRegionMap m_timeRegionMap; + + // Set of registered threads when created + AZStd::vector, AZ::OSStdAllocator> m_registeredThreads; + AZStd::mutex m_threadRegisterMutex; + + // Thread local storage, gets lazily allocated when a thread is created + static thread_local CpuTimingLocalStorage* ms_threadLocalStorage; + + // Enable/Disables the threads from profiling + AZStd::atomic_bool m_enabled = false; + + // This lock will only be contested when the CpuProfiler's Shutdown() method has been called + AZStd::shared_mutex m_shutdownMutex; + + bool m_initialized = false; + + AZStd::mutex m_continuousCaptureEndingMutex; + + AZStd::atomic_bool m_continuousCaptureInProgress; + + // Stores multiple frames of profiling data, size is controlled by MaxFramesToSave. Flushed when EndContinuousCapture is called. + // Ring buffer so that we can have fast append of new data + removal of old profiling data with good cache locality. + AZStd::ring_buffer m_continuousCaptureData; + }; + + // Intermediate class to serialize Cpu TimedRegion data. + class CpuProfilingStatisticsSerializer + { + public: + class CpuProfilingStatisticsSerializerEntry + { + public: + AZ_TYPE_INFO(CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry, "{26B78F65-EB96-46E2-BE7E-A1233880B225}"); + static void Reflect(AZ::ReflectContext* context); + + CpuProfilingStatisticsSerializerEntry() = default; + CpuProfilingStatisticsSerializerEntry(const CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId); + + AZ::Name m_groupName; + AZ::Name m_regionName; + uint16_t m_stackDepth; + AZStd::sys_time_t m_startTick; + AZStd::sys_time_t m_endTick; + size_t m_threadId; + }; + + AZ_TYPE_INFO(CpuProfilingStatisticsSerializer, "{D5B02946-0D27-474F-9A44-364C2706DD41}"); + static void Reflect(AZ::ReflectContext* context); + + CpuProfilingStatisticsSerializer() = default; + CpuProfilingStatisticsSerializer(const AZStd::ring_buffer& continuousData); + + AZStd::vector m_cpuProfilingStatisticsSerializerEntries; + }; +}; // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp new file mode 100644 index 0000000000..3f364f99e0 --- /dev/null +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.cpp @@ -0,0 +1,1150 @@ +/* + * 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 + * + */ + +#if defined(IMGUI_ENABLED) + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Profiler +{ + static constexpr const char* defaultSaveLocation = "@user@/Profiler"; + + namespace CpuProfilerImGuiHelper + { + float TicksToMs(double ticks) + { + // Note: converting to microseconds integer before converting to milliseconds float + const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); + AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000"); + return static_cast((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f; + } + + float TicksToMs(AZStd::sys_time_t ticks) + { + return TicksToMs(static_cast(ticks)); + } + + using DeserializedCpuData = AZStd::vector; + + AZ::Outcome LoadSavedCpuProfilingStatistics(const char* capturePath) + { + auto* base = AZ::IO::FileIOBase::GetInstance(); + + char resolvedPath[AZ::IO::MaxPathLength]; + if (!base->ResolvePath(capturePath, resolvedPath, AZ::IO::MaxPathLength)) + { + return AZ::Failure(AZStd::string::format("Could not resolve the path to file %s, is the path correct?", resolvedPath)); + } + + AZ::u64 captureSizeBytes; + const AZ::IO::Result fileSizeResult = base->Size(resolvedPath, captureSizeBytes); + if (!fileSizeResult) + { + return AZ::Failure(AZStd::string::format("Could not read the size of file %s, is the path correct?", resolvedPath)); + } + + // NOTE: this uses raw file pointers over the abstractions and utility functions provided by AZ::JsonSerializationUtils because + // saved profiling captures can be upwards of 400 MB. This necessitates a buffered approach to avoid allocating huge chunks of memory. + FILE* fp = nullptr; + azfopen(&fp, resolvedPath, "rb"); + if (!fp) + { + return AZ::Failure(AZStd::string::format("Could not fopen file %s, is the path correct?\n", resolvedPath)); + } + + constexpr AZStd::size_t MaxBufSize = 65536; + const AZStd::size_t bufSize = AZStd::min(MaxBufSize, aznumeric_cast(captureSizeBytes)); + char* buf = reinterpret_cast(azmalloc(bufSize)); + + rapidjson::Document document; + rapidjson::FileReadStream inputStream(fp, buf, bufSize); + document.ParseStream(inputStream); + + azfree(buf); + fclose(fp); + + if (document.HasParseError()) + { + const auto pe = document.GetParseError(); + return AZ::Failure(AZStd::string::format( + "Rapidjson could not parse the document with ParseErrorCode %u. See 3rdParty/rapidjson/error.h for definitions.\n", pe)); + } + + if (!document.IsObject() || !document.HasMember("ClassData")) + { + return AZ::Failure(AZStd::string::format( + "Error in loading saved capture: top-level object does not have a ClassData field. Did the serialization format change recently?\n")); + } + + AZ_TracePrintf("JsonUtils", "Successfully loaded JSON into memory.\n"); + + const auto& root = document["ClassData"]; + CpuProfilingStatisticsSerializer serializer; + const AZ::JsonSerializationResult::ResultCode deserializationResult = AZ::JsonSerialization::Load(serializer, root); + if (deserializationResult.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted + || serializer.m_cpuProfilingStatisticsSerializerEntries.empty()) + { + return AZ::Failure(AZStd::string::format("Error in deserializing document: %s\n", deserializationResult.ToString(capturePath).c_str())); + } + + AZ_TracePrintf("JsonUtils", "Successfully loaded CPU profiling data with %zu profiling entries.\n", + serializer.m_cpuProfilingStatisticsSerializerEntries.size()); + + return AZ::Success(AZStd::move(serializer.m_cpuProfilingStatisticsSerializerEntries)); + } + } // namespace CpuProfilerImGuiHelper + + void ImGuiCpuProfiler::Draw(bool& keepDrawing) + { + // Cache the value to detect if it was changed by ImGui(user pressed 'x') + const bool cachedShowCpuProfiler = keepDrawing; + + const ImVec2 windowSize(900.0f, 600.0f); + ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once); + if (ImGui::Begin("CPU Profiler", &keepDrawing, ImGuiWindowFlags_None)) + { + // Collect the last frame's profiling data + if (!m_paused) + { + // Update region map and cache the input cpu timing statistics when the profiling is not paused + CacheCpuTimingStatistics(); + + CollectFrameData(); + CullFrameData(); + + // Only listen to system ticks when the profiler is active + if (!AZ::SystemTickBus::Handler::BusIsConnected()) + { + AZ::SystemTickBus::Handler::BusConnect(); + } + } + + if (m_enableVisualizer) + { + DrawVisualizer(); + } + else + { + DrawStatisticsView(); + } + + if (m_showFilePicker) + { + DrawFilePicker(); + } + } + ImGui::End(); + + if (m_captureToFile) + { + AZStd::string timeString; + AZStd::to_string(timeString, AZStd::GetTimeNowSecond()); + + const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_single_%s.json", defaultSaveLocation, timeString.c_str()); + + char resolvedPath[AZ::IO::MaxPathLength]; + AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); + m_lastCapturedFilePath = resolvedPath; + + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath); + } + m_captureToFile = false; + + // Toggle if the bool isn't the same as the cached value + if (cachedShowCpuProfiler != keepDrawing) + { + CpuProfiler::Get()->SetProfilerEnabled(keepDrawing); + } + } + + void ImGuiCpuProfiler::DrawCommonHeader() + { + if (!m_lastCapturedFilePath.empty()) + { + ImGui::Text("Saved: %s", m_lastCapturedFilePath.c_str()); + } + + if (ImGui::Button(m_enableVisualizer ? "Swap to statistics" : "Swap to visualizer")) + { + m_enableVisualizer = !m_enableVisualizer; + } + + ImGui::SameLine(); + m_paused = !CpuProfiler::Get()->IsProfilerEnabled(); + if (ImGui::Button(m_paused ? "Resume" : "Pause")) + { + m_paused = !m_paused; + CpuProfiler::Get()->SetProfilerEnabled(!m_paused); + } + + ImGui::SameLine(); + if (ImGui::Button("Capture")) + { + m_captureToFile = true; + } + + ImGui::SameLine(); + bool isInProgress = CpuProfiler::Get()->IsContinuousCaptureInProgress(); + if (ImGui::Button(isInProgress ? "End" : "Begin")) + { + if (isInProgress) + { + AZStd::string timeString; + AZStd::to_string(timeString, AZStd::GetTimeNowSecond()); + + const AZStd::string frameDataFilePath = AZStd::string::format("%s/cpu_multi_%s.json", defaultSaveLocation, timeString.c_str()); + + char resolvedPath[AZ::IO::MaxPathLength]; + AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength); + m_lastCapturedFilePath = resolvedPath; + + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::EndContinuousCpuProfilingCapture, frameDataFilePath); + + m_paused = true; + } + else + { + ProfilerRequestBus::Broadcast(&ProfilerRequestBus::Events::BeginContinuousCpuProfilingCapture); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Load file")) + { + m_showFilePicker = true; + + // Only update the cached file list when opened so that we aren't making IO calls on every frame. + m_cachedCapturePaths.clear(); + + auto* base = AZ::IO::FileIOBase::GetInstance(); + base->FindFiles(defaultSaveLocation, "*.json", + [&paths = m_cachedCapturePaths](const char* path) -> bool + { + auto foundPath = AZ::IO::Path(path); + paths.push_back(foundPath); + return true; + }); + + // Sort by decreasing modification time (most recent at the top) + AZStd::sort(m_cachedCapturePaths.begin(), m_cachedCapturePaths.end(), + [&base](const AZ::IO::Path& lhs, const AZ::IO::Path& rhs) + { + return base->ModificationTime(lhs.c_str()) > base->ModificationTime(rhs.c_str()); + }); + } + } + + void ImGuiCpuProfiler::DrawTable() + { + const auto flags = + ImGuiTableFlags_Borders | ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable; + if (ImGui::BeginTable("FunctionStatisticsTable", 6, flags)) + { + // Table header setup + ImGui::TableSetupColumn("Group"); + ImGui::TableSetupColumn("Region"); + ImGui::TableSetupColumn("MTPC (ms)"); + ImGui::TableSetupColumn("Max (ms)"); + ImGui::TableSetupColumn("Invocations"); + ImGui::TableSetupColumn("Total (ms)"); + ImGui::TableHeadersRow(); + ImGui::TableNextColumn(); + + ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs(); + if (sortSpecs && sortSpecs->SpecsDirty) + { + SortTable(sortSpecs); + } + + // Draw all of the rows held in the GroupRegionMap + for (const auto* statistics : m_tableData) + { + if (!m_timedRegionFilter.PassFilter(statistics->m_groupName.c_str()) + && !m_timedRegionFilter.PassFilter(statistics->m_regionName.c_str())) + { + continue; + } + + ImGui::Text("%s", statistics->m_groupName.c_str()); + const ImVec2 topLeftBound = ImGui::GetItemRectMin(); + ImGui::TableNextColumn(); + + ImGui::Text("%s", statistics->m_regionName.c_str()); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_runningAverageTicks)); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_maxTicks)); + ImGui::TableNextColumn(); + + ImGui::Text("%llu", statistics->m_invocationsLastFrame); + ImGui::TableNextColumn(); + + ImGui::Text("%.2f", CpuProfilerImGuiHelper::TicksToMs(statistics->m_lastFrameTotalTicks)); + const ImVec2 botRightBound = ImGui::GetItemRectMax(); + ImGui::TableNextColumn(); + + // NOTE: we are manually checking the bounds rather than using ImGui::IsItemHovered + Begin/EndGroup because + // ImGui reports incorrect bounds when using Begin/End group in the Tables API. + if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(topLeftBound, botRightBound, false)) + { + ImGui::BeginTooltip(); + ImGui::Text("%s", statistics->GetExecutingThreadsLabel().c_str()); + ImGui::EndTooltip(); + } + } + } + ImGui::EndTable(); + } + + void ImGuiCpuProfiler::SortTable(ImGuiTableSortSpecs* sortSpecs) + { + const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; + const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; + + switch (columnToSort) + { + case (0): // Sort by group name + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_groupName, ascending)); + break; + case (1): // Sort by region name + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_regionName, ascending)); + break; + case (2): // Sort by average time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_runningAverageTicks, ascending)); + break; + case (3): // Sort by max time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_maxTicks, ascending)); + break; + case (4): // Sort by invocations + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending)); + break; + case (5): // Sort by total time + AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending)); + break; + } + sortSpecs->SpecsDirty = false; + } + + void ImGuiCpuProfiler::DrawStatisticsView() + { + DrawCommonHeader(); + + const auto ShowRow = [](const char* regionLabel, double duration) + { + ImGui::Text("%s", regionLabel); + ImGui::NextColumn(); + + ImGui::Text("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(duration)); + ImGui::NextColumn(); + }; + + if (ImGui::BeginChild("Statistics View", { 0, 0 }, true)) + { + // Set column settings. + ImGui::Columns(2, "view", false); + ImGui::SetColumnWidth(0, 660.0f); + ImGui::SetColumnWidth(1, 100.0f); + + for (const auto& queueStatistics : m_cpuTimingStatisticsWhenPause) + { + ShowRow(queueStatistics.m_name.c_str(), queueStatistics.m_executeDuration); + } + + ImGui::Separator(); + ImGui::Columns(1, "view", false); + + m_timedRegionFilter.Draw("Filter"); + ImGui::SameLine(); + if (ImGui::Button("Clear Filter")) + { + m_timedRegionFilter.Clear(); + } + ImGui::SameLine(); + if (ImGui::Button("Reset Table")) + { + m_tableData.clear(); + m_groupRegionMap.clear(); + } + + DrawTable(); + } + } + + void ImGuiCpuProfiler::DrawFilePicker() + { + ImGui::SetNextWindowSize({ 500, 200 }, ImGuiCond_Once); + if (ImGui::Begin("File Picker", &m_showFilePicker)) + { + if (ImGui::Button("Load selected")) + { + LoadFile(); + } + + auto getter = [](void* vectorPointer, int idx, const char** out_text) -> bool + { + const auto& pathVec = *static_cast*>(vectorPointer); + if (idx < 0 || idx >= pathVec.size()) + { + return false; + } + *out_text = pathVec[idx].c_str(); + return true; + }; + + ImGui::SetNextItemWidth(ImGui::GetWindowContentRegionWidth()); + ImGui::ListBox("", &m_currentFileIndex, getter, &m_cachedCapturePaths, aznumeric_cast(m_cachedCapturePaths.size())); + } + ImGui::End(); + } + + void ImGuiCpuProfiler::LoadFile() + { + const AZ::IO::Path& pathToLoad = m_cachedCapturePaths[m_currentFileIndex]; + auto loadResult = CpuProfilerImGuiHelper::LoadSavedCpuProfilingStatistics(pathToLoad.c_str()); + if (!loadResult.IsSuccess()) + { + AZ_TracePrintf("ImGuiCpuProfiler", "%s", loadResult.GetError().c_str()); + return; + } + + AZStd::vector deserializedData = loadResult.TakeValue(); + + // Clear visualizer and statistics view state + m_savedRegionCount = deserializedData.size(); + m_savedData.clear(); + m_paused = true; + + CpuProfiler::Get()->SetProfilerEnabled(false); + m_frameEndTicks.clear(); + + m_tableData.clear(); + m_groupRegionMap.clear(); + + for (const auto& entry : deserializedData) + { + const auto [groupNameItr, wasGroupNameInserted] = m_deserializedStringPool.emplace(entry.m_groupName.GetCStr()); + const auto [regionNameItr, wasRegionNameInserted] = m_deserializedStringPool.emplace(entry.m_regionName.GetCStr()); + const auto [groupRegionNameItr, wasGroupRegionNameInserted] = + m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str()); + + const CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick); + m_savedData[entry.m_threadId].push_back(newRegion); + + // Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic. + const static AZ::Name frameBoundaryName = AZ::Name("RPISystem: OnSystemTick"); + if (entry.m_regionName == frameBoundaryName) + { + m_frameEndTicks.push_back(entry.m_endTick); + } + + // Update running statistics + if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr)) + { + m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr; + m_groupRegionMap[*groupNameItr][*regionNameItr].m_regionName = *regionNameItr; + m_tableData.push_back(&m_groupRegionMap[*groupNameItr][*regionNameItr]); + } + m_groupRegionMap[*groupNameItr][*regionNameItr].RecordRegion(newRegion, entry.m_threadId); + } + + // Update viewport bounds with some added UX fudge factor + m_viewportStartTick = deserializedData.back().m_startTick - 1000; + m_viewportEndTick = deserializedData.back().m_endTick + 1000; + + // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data. + for (auto& [threadId, singleThreadData] : m_savedData) + { + AZStd::sort(singleThreadData.begin(), singleThreadData.end(), + [](const TimeRegion& lhs, const TimeRegion& rhs) + { + return lhs.m_startTick < rhs.m_startTick; + }); + } + } + + // -- CPU Visualizer -- + void ImGuiCpuProfiler::DrawVisualizer() + { + DrawCommonHeader(); + + // Options & Statistics + if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true)) + { + ImGui::Columns(3, "Options", true); + ImGui::SliderInt("Saved Frames", &m_framesToCollect, 10, 20000, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic); + m_visualizerHighlightFilter.Draw("Find Region"); + + ImGui::NextColumn(); + + ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth())); + ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick); + ImGui::Text("Recording %zu threads", m_savedData.size()); + ImGui::Text("%llu profiling events saved", m_savedRegionCount); + + ImGui::NextColumn(); + + ImGui::TextWrapped( + "Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding ."); + } + + ImGui::Columns(1, "FrameTimeColumn", true); + + if (ImGui::BeginChild("FrameTimeHistogram", { 0, 50 }, true, ImGuiWindowFlags_NoScrollbar)) + { + DrawFrameTimeHistogram(); + } + ImGui::EndChild(); + + ImGui::Columns(1, "RulerColumn", true); + + // Ruler + if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus)) + { + DrawRuler(); + } + ImGui::EndChild(); + + + ImGui::Columns(1, "TimelineColumn", true); + + // Timeline + if (ImGui::BeginChild( + "Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + // Find the next frame boundary after the viewport's right bound and draw until that tick + auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick); + if (nextFrameBoundaryItr == m_frameEndTicks.end() && m_frameEndTicks.size() != 0) + { + --nextFrameBoundaryItr; + } + const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr; + + // Find the start tick of the leftmost frame, which may be offscreen. + auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + if (startTickItr != m_frameEndTicks.begin()) + { + --startTickItr; + } + + // Main draw loop + AZ::u64 baseRow = 0; + for (const auto& [currentThreadId, singleThreadData] : m_savedData) + { + // Find the first TimeRegion that we should draw + auto regionItr = AZStd::lower_bound( + singleThreadData.begin(), singleThreadData.end(), *startTickItr, + [](const TimeRegion& wrapper, AZStd::sys_time_t target) + { + return wrapper.m_startTick < target; + }); + + if (regionItr == singleThreadData.end()) + { + continue; + } + + // Draw all of the blocks for a given thread/row + AZ::u64 maxDepth = 0; + while (regionItr != singleThreadData.end()) + { + const TimeRegion& region = *regionItr; + + // Early out if we have drawn all the onscreen regions + if (region.m_startTick > nextFrameBoundary) + { + break; + } + AZ::u64 targetRow = region.m_stackDepth + baseRow; + maxDepth = AZStd::max(aznumeric_cast(region.m_stackDepth), maxDepth); + + DrawBlock(region, targetRow); + + ++regionItr; + } + + // Draw UI details + DrawThreadLabel(baseRow, currentThreadId); + DrawThreadSeparator(baseRow, maxDepth); + + baseRow += maxDepth + 1; // Next draw loop should start one row down + } + + DrawFrameBoundaries(); + + // Draw an invisible button to capture inputs + ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight }); + + // Controls + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsWindowFocused() && ImGui::IsItemHovered()) + { + io.WantCaptureMouse = true; + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling + { + const auto [deltaX, deltaY] = io.MouseDelta; + if (deltaX != 0 || deltaY != 0) + { + // We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot + // relative to the objects on screen) + const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth(); + auto tickDelta = aznumeric_cast(-1 * pixelDeltaNormalized * GetViewportTickWidth()); + m_viewportStartTick += tickDelta; + m_viewportEndTick += tickDelta; + + ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1); + } + } + else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming + { + // We want zooming to be relative to the mouse's current position + const float mouseX = ImGui::GetMousePos().x; + + // Find the normalized position of the cursor relative to the window + const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth(); + + const auto overallTickDelta = aznumeric_cast(0.05 * io.MouseWheel * GetViewportTickWidth()); + + // Split the overall delta between the two bounds depending on mouse pos + const auto newStartTick = m_viewportStartTick + aznumeric_cast(percentWindow * overallTickDelta); + const auto newEndTick = m_viewportEndTick - aznumeric_cast((1-percentWindow) * overallTickDelta); + + // Avoid zooming too much, start tick should always be less than end tick + if (newStartTick < newEndTick) + { + m_viewportStartTick = newStartTick; + m_viewportEndTick = newEndTick; + } + } + } + } + ImGui::EndChild(); + } + + void ImGuiCpuProfiler::CacheCpuTimingStatistics() + { + using namespace AZ::Statistics; + + m_cpuTimingStatisticsWhenPause.clear(); + if (auto statsProfiler = AZ::Interface::Get(); statsProfiler) + { + auto& rhiMetrics = statsProfiler->GetProfiler(AZ_CRC_CE("RHI")); + + const NamedRunningStatistic* frameTimeMetric = rhiMetrics.GetStatistic(AZ_CRC_CE("Frame to Frame Time")); + if (frameTimeMetric) + { + m_frameToFrameTime = static_cast(frameTimeMetric->GetMostRecentSample()); + } + + AZStd::vector statistics; + rhiMetrics.GetStatsManager().GetAllStatistics(statistics); + + for (NamedRunningStatistic* stat : statistics) + { + m_cpuTimingStatisticsWhenPause.push_back({ stat->GetName(), stat->GetMostRecentSample() }); + stat->Reset(); + } + } + } + + void ImGuiCpuProfiler::CollectFrameData() + { + // We maintain separate datastores for the visualizer and the statistical view because they require different + // data formats - one grouped by thread ID versus the other organized by group + region. Since the statistical + // view is only holding data from the last frame, the memory overhead is minimal and gives us a faster redraw + // compared to if we needed to transform the visualizer's data into the statistical format every frame. + + // Get the latest TimeRegionMap + const CpuProfiler::TimeRegionMap& timeRegionMap = CpuProfiler::Get()->GetTimeRegionMap(); + + m_viewportStartTick = AZStd::numeric_limits::max(); + m_viewportEndTick = AZStd::numeric_limits::lowest(); + + // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame + for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap) + { + const size_t threadIdHashed = AZStd::hash{}(threadId); + // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) + if (singleThreadRegionMap.size() == 0) + { + continue; + } + + // Now focus on just the data for the current thread + AZStd::vector newVisualizerData; + newVisualizerData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation + for (const auto& [regionName, regionVec] : singleThreadRegionMap) + { + for (const TimeRegion& region : regionVec) + { + newVisualizerData.push_back(region); // Copies + + // Also update the statistical view's data + const AZStd::string& groupName = region.m_groupRegionName.m_groupName; + + if (!m_groupRegionMap[groupName].contains(regionName)) + { + m_groupRegionMap[groupName][regionName].m_groupName = groupName; + m_groupRegionMap[groupName][regionName].m_regionName = regionName; + m_tableData.push_back(&m_groupRegionMap[groupName][regionName]); + } + + m_groupRegionMap[groupName][regionName].RecordRegion(region, threadIdHashed); + } + } + + // Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw) + // since we can binary search by start tick. + AZStd::sort( + newVisualizerData.begin(), newVisualizerData.end(), + [](const TimeRegion& lhs, const TimeRegion& rhs) + { + return lhs.m_startTick < rhs.m_startTick; + }); + + // Use the latest frame's data as the new bounds of the viewport + m_viewportStartTick = AZStd::min(newVisualizerData.front().m_startTick, m_viewportStartTick); + m_viewportEndTick = AZStd::max(newVisualizerData.back().m_endTick, m_viewportEndTick); + + m_savedRegionCount += newVisualizerData.size(); + + // Move onto the end of the current thread's saved data, sorted order maintained + AZStd::vector& savedDataVec = m_savedData[threadIdHashed]; + savedDataVec.insert( + savedDataVec.end(), AZStd::make_move_iterator(newVisualizerData.begin()), AZStd::make_move_iterator(newVisualizerData.end())); + } + } + + void ImGuiCpuProfiler::CullFrameData() + { + const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - m_frameToFrameTime * m_framesToCollect; + + // Remove old frame boundary data + auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick); + m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr); + + // Remove old region data for each thread + for (auto& [threadId, savedRegions] : m_savedData) + { + AZStd::size_t sizeBeforeRemove = savedRegions.size(); + + // Early out to avoid the linear erase_if call + if (savedRegions.size() >= 1 && savedRegions.at(0).m_startTick > deleteBeforeTick) + { + continue; + } + + // Use erase_if over plain upper_bound + erase to avoid repeated shifts. erase requires a shift of all elements to the right + // for each element that is erased, while erase_if squashes all removes into a single shift which significantly improves perf. + AZStd::erase_if( + savedRegions, + [deleteBeforeTick](const TimeRegion& region) + { + return region.m_startTick < deleteBeforeTick; + }); + + m_savedRegionCount -= sizeBeforeRemove - savedRegions.size(); + } + + // Remove any threads from the top-level map that no longer hold data + AZStd::erase_if( + m_savedData, + [](const auto& singleThreadDataEntry) + { + return singleThreadDataEntry.second.empty(); + }); + } + + void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, AZ::u64 targetRow) + { + // Don't draw anything if the user is searching for regions and this block doesn't pass the filter + if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName)) + { + return; + } + + float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY(); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float startPixel = ConvertTickToPixelSpace(block.m_startTick, m_viewportStartTick, m_viewportEndTick); + const float endPixel = ConvertTickToPixelSpace(block.m_endTick, m_viewportStartTick, m_viewportEndTick); + + if (endPixel - startPixel < 0.5f) + { + return; + } + + const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight + 1}; + const ImVec2 endPoint = { endPixel, wy + (targetRow + 1) * RowHeight }; + + const ImU32 blockColor = GetBlockColor(block); + + drawList->AddRectFilled(startPoint, endPoint, blockColor, 0); + drawList->AddLine(startPoint, { endPixel, startPoint.y }, IM_COL32_BLACK, 0.5f); + drawList->AddLine({ startPixel, endPoint.y }, endPoint, IM_COL32_BLACK, 0.5f); + + // Draw the region name if possible + // If the block's current width is too small, we skip drawing the label. + const float regionPixelWidth = endPixel - startPixel; + const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em) + if (regionPixelWidth > maxCharWidth) // We can draw at least one character + { + const AZStd::string label = + AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); + const float textWidth = ImGui::CalcTextSize(label.c_str()).x; + + if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. + { + const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y }; + + // NOTE: RenderText calls do not automatically account for the global scale (which is modified at high DPI) + // so we must adjust for the scale manually. + const float scaleFactor = ImGui::GetIO().FontGlobalScale; + const float fontSize = ImGui::GetFont()->FontSize * scaleFactor; + + ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); + } + else // We have enough space to draw the entire label, draw and center text. + { + const float remainingWidth = regionPixelWidth - textWidth; + const float offset = remainingWidth * .5f; + + drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str()); + } + } + + // Tooltip and block highlighting + if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered()) + { + // Go to the statistics view when a region is clicked + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + m_enableVisualizer = false; + const auto newFilter = AZStd::string(block.m_groupRegionName.m_regionName); + m_timedRegionFilter = ImGuiTextFilter(newFilter.c_str()); + m_timedRegionFilter.Build(); + } + // Hovering outline + drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); + + ImGui::BeginTooltip(); + ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); + ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); + ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); + ImGui::EndTooltip(); + } + } + + ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) + { + // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers + const GroupRegionName& key = block.m_groupRegionName; + if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit + { + return ImGui::GetColorU32(iter->second); + } + + // Cache miss, generate a new random color + AZ::SimpleLcgRandom rand(aznumeric_cast(AZStd::GetTimeNowTicks())); + const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f); + const ImVec4 randomColor = {r, g, b, .8}; + m_regionColorMap.emplace(key, randomColor); + return ImGui::GetColorU32(randomColor); + } + + void ImGuiCpuProfiler::DrawThreadSeparator(AZ::u64 baseRow, AZ::u64 maxDepth) + { + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const float windowWidth = ImGui::GetWindowWidth(); + const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight; + + ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 1.0f); + } + + void ImGuiCpuProfiler::DrawThreadLabel(AZ::u64 baseRow, size_t threadId) + { + auto [wx, wy] = ImGui::GetWindowPos(); + wy -= ImGui::GetScrollY(); + const AZStd::string threadIdText = AZStd::string::format("Thread: %zu", threadId); + + ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight}, IM_COL32_WHITE, threadIdText.c_str()); + } + + void ImGuiCpuProfiler::DrawFrameBoundaries() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + const float wy = ImGui::GetWindowPos().y; + const float windowHeight = ImGui::GetWindowHeight(); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + // End ticks are sorted in increasing order, find the first frame bound to draw + auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + + while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick) + { + const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr, m_viewportStartTick, m_viewportEndTick); + drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red); + ++endTickItr; + } + } + + void ImGuiCpuProfiler::DrawRuler() + { + // Use a pair of iterators to go through all saved frame boundaries and draw ruler lines + auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick); + auto nextFrameBoundaryItr = lastFrameBoundaryItr; + if (lastFrameBoundaryItr != m_frameEndTicks.begin()) + { + --lastFrameBoundaryItr; + } + + const auto [wx, wy] = ImGui::GetWindowPos(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + while (nextFrameBoundaryItr != m_frameEndTicks.end() && *lastFrameBoundaryItr <= m_viewportEndTick) + { + const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr; + const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr; + if (lastFrameBoundaryTick > m_viewportEndTick) + { + break; + } + + const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick, m_viewportStartTick, m_viewportEndTick); + + const AZStd::string label = + AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick)); + const float labelWidth = ImGui::CalcTextSize(label.c_str()).x; + + // The label can fit between the two boundaries, center it and draw + if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel) + { + const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2; + const float textBeginPixel = lastFrameBoundaryPixel + offset; + const float textEndPixel = textBeginPixel + labelWidth; + + const float verticalOffset = (ImGui::GetWindowHeight() - ImGui::GetFontSize()) / 2; + + // Execution time label + drawList->AddText({ textBeginPixel, wy + verticalOffset }, IM_COL32_WHITE, label.c_str()); + + // Left side + drawList->AddLine( + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2}, + IM_COL32_WHITE); + + // Right side + drawList->AddLine( + { textEndPixel, wy + ImGui::GetWindowHeight()/2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 }, + IM_COL32_WHITE); + } + else // Cannot fit inside, just draw a line between the two boundaries + { + drawList->AddLine( + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 }, + IM_COL32_WHITE); + } + + // Left bound + drawList->AddLine( + { lastFrameBoundaryPixel, wy }, + { lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + // Right bound + drawList->AddLine( + { nextFrameBoundaryPixel, wy }, + { nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() }, + IM_COL32_WHITE); + + lastFrameBoundaryItr = nextFrameBoundaryItr; + ++nextFrameBoundaryItr; + } + } + + void ImGuiCpuProfiler::DrawFrameTimeHistogram() + { + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const auto [wx, wy] = ImGui::GetWindowPos(); + const ImU32 orange = ImGui::GetColorU32({ 1, .7, 0, 1 }); + const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 }); + + const AZStd::sys_time_t ticksPerSecond = AZStd::GetTimeTicksPerSecond(); + const AZStd::sys_time_t viewportCenter = m_viewportEndTick - (m_viewportEndTick - m_viewportStartTick) / 2; + const AZStd::sys_time_t leftHistogramBound = viewportCenter - ticksPerSecond; + const AZStd::sys_time_t rightHistogramBound = viewportCenter + ticksPerSecond; + + // Draw frame limit lines + drawList->AddLine( + { wx, wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, + { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - MediumFrameTimeLimit }, + orange); + + drawList->AddLine( + { wx, wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, + { wx + ImGui::GetWindowWidth(), wy + ImGui::GetWindowHeight() - HighFrameTimeLimit }, + red); + + + // Draw viewport bound rectangle + const float leftViewportPixel = ConvertTickToPixelSpace(m_viewportStartTick, leftHistogramBound, rightHistogramBound); + const float rightViewportPixel = ConvertTickToPixelSpace(m_viewportEndTick, leftHistogramBound, rightHistogramBound); + const ImVec2 topLeftPos = { leftViewportPixel, wy }; + const ImVec2 botRightPos = { rightViewportPixel, wy + ImGui::GetWindowHeight() }; + const ImU32 gray = ImGui::GetColorU32({ 1, 1, 1, .3 }); + drawList->AddRectFilled(topLeftPos, botRightPos, gray); + + // Find the first onscreen frame execution time + auto frameEndTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), leftHistogramBound); + if (frameEndTickItr != m_frameEndTicks.begin()) + { + --frameEndTickItr; + } + + // Since we only store the frame end ticks, we must calculate the execution times on the fly by comparing pairs of elements. + AZStd::sys_time_t lastFrameEndTick = *frameEndTickItr; + while (*frameEndTickItr < rightHistogramBound && ++frameEndTickItr != m_frameEndTicks.end()) + { + const AZStd::sys_time_t frameEndTick = *frameEndTickItr; + + const float framePixelPos = ConvertTickToPixelSpace(frameEndTick, leftHistogramBound, rightHistogramBound); + const float frameTimeMs = CpuProfilerImGuiHelper::TicksToMs(frameEndTick - lastFrameEndTick); + + const ImVec2 lineBottom = { framePixelPos, ImGui::GetWindowHeight() + wy }; + const ImVec2 lineTop = { framePixelPos, ImGui::GetWindowHeight() + wy - frameTimeMs }; + + ImU32 lineColor = ImGui::GetColorU32({ .3, .3, .3, 1 }); // Gray + if (frameTimeMs > HighFrameTimeLimit) + { + lineColor = ImGui::GetColorU32({1, 0, 0, 1}); // Red + } + else if (frameTimeMs > MediumFrameTimeLimit) + { + lineColor = ImGui::GetColorU32({1, .7, 0, 1}); // Orange + } + + drawList->AddLine(lineBottom, lineTop, lineColor, 3.0); + + lastFrameEndTick = frameEndTick; + } + + // Handle input + ImGui::InvisibleButton("HistogramInputCapture", { ImGui::GetWindowWidth(), ImGui::GetWindowHeight() }); + ImGuiIO& io = ImGui::GetIO(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + const float mousePixelX = io.MousePos.x; + const float percentWindow = (mousePixelX - wx) / ImGui::GetWindowWidth(); + const AZStd::sys_time_t newViewportCenterTick = leftHistogramBound + + aznumeric_cast((rightHistogramBound - leftHistogramBound) * percentWindow); + + const AZStd::sys_time_t viewportWidth = GetViewportTickWidth(); + m_viewportEndTick = newViewportCenterTick + viewportWidth / 2; + m_viewportStartTick = newViewportCenterTick - viewportWidth / 2; + } + } + + AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const + { + return m_viewportEndTick - m_viewportStartTick; + } + + float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const + { + const float wx = ImGui::GetWindowPos().x; + const float tickSpaceShifted = aznumeric_cast(tick - leftBound); // This will be close to zero, so FP inaccuracy should not be too bad + const float tickSpaceNormalized = tickSpaceShifted / (rightBound - leftBound); + const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx; + return pixelSpace; + } + + // System tick bus overrides + void ImGuiCpuProfiler::OnSystemTick() + { + if (m_paused) + { + AZ::SystemTickBus::Handler::BusDisconnect(); + } + else + { + m_frameEndTicks.push_back(AZStd::GetTimeNowTicks()); + + for (auto& [groupName, regionMap] : m_groupRegionMap) + { + for (auto& [regionName, row] : regionMap) + { + row.ResetPerFrameStatistics(); + } + } + } + } + + // ---- TableRow impl ---- + + void TableRow::RecordRegion(const CachedTimeRegion& region, size_t threadId) + { + const AZStd::sys_time_t deltaTime = region.m_endTick - region.m_startTick; + + // Update per frame statistics + ++m_invocationsLastFrame; + m_executingThreads.insert(threadId); + m_lastFrameTotalTicks += deltaTime; + m_maxTicks = AZStd::max(m_maxTicks, deltaTime); + + // Update aggregate statistics + m_runningAverageTicks = + aznumeric_cast((1.0 * (deltaTime + m_invocationsTotal * m_runningAverageTicks)) / (m_invocationsTotal + 1)); + ++m_invocationsTotal; + } + + void TableRow::ResetPerFrameStatistics() + { + m_invocationsLastFrame = 0; + m_executingThreads.clear(); + m_lastFrameTotalTicks = 0; + m_maxTicks = 0; + } + + AZStd::string TableRow::GetExecutingThreadsLabel() const + { + auto threadString = AZStd::string::format("Executed in %zu threads\n", m_executingThreads.size()); + for (const auto& threadId : m_executingThreads) + { + threadString.append(AZStd::string::format("Thread: %zu\n", threadId)); + } + return threadString; + } +} // namespace Profiler + +#endif // defined(IMGUI_ENABLED) diff --git a/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h new file mode 100644 index 0000000000..2c6a3e470a --- /dev/null +++ b/Gems/Profiler/Code/Source/ImGuiCpuProfiler.h @@ -0,0 +1,234 @@ +/* + * 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 + +#if defined(IMGUI_ENABLED) + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace Profiler +{ + //! Stores all the data associated with a row in the table. + struct TableRow + { + template + struct TableRowCompareFunctor + { + TableRowCompareFunctor(T memberPointer, bool isAscending) : m_memberPointer(memberPointer), m_ascending(isAscending){}; + + bool operator()(const TableRow* lhs, const TableRow* rhs) + { + return m_ascending ? lhs->*m_memberPointer < rhs->*m_memberPointer : lhs->*m_memberPointer > rhs->*m_memberPointer; + } + + T m_memberPointer; + bool m_ascending; + }; + + // Update running statistics with new region data + void RecordRegion(const CachedTimeRegion& region, size_t threadId); + + void ResetPerFrameStatistics(); + + // Get a string of all threads that this region executed in during the last frame + AZStd::string GetExecutingThreadsLabel() const; + + AZStd::string m_groupName; + AZStd::string m_regionName; + + // --- Per frame statistics --- + + AZ::u64 m_invocationsLastFrame = 0; + + // NOTE: set over unordered_set so the threads can be shown in increasing order in tooltip. + AZStd::set m_executingThreads; + + AZStd::sys_time_t m_lastFrameTotalTicks = 0; + + // Maximum execution time of a region in the last frame. + AZStd::sys_time_t m_maxTicks = 0; + + // --- Aggregate statistics --- + + AZ::u64 m_invocationsTotal = 0; + + // Running average of Mean Time Per Call + AZStd::sys_time_t m_runningAverageTicks = 0; + }; + + //! ImGui widget for examining CPU Profiling instrumentation. + //! Offers both a statistical view (with sorting and searching capability) and a visualizer + //! similar to other profiling tools. + class ImGuiCpuProfiler + : public AZ::SystemTickBus::Handler + { + // Region Name -> statistical view row data + using RegionRowMap = AZStd::map; + // Group Name -> RegionRowMap + using GroupRegionMap = AZStd::map; + + using TimeRegion = CachedTimeRegion; + using GroupRegionName = CachedTimeRegion::GroupRegionName; + + public: + struct CpuTimingEntry + { + const AZStd::string& m_name; + double m_executeDuration; + }; + + ImGuiCpuProfiler() = default; + ~ImGuiCpuProfiler() = default; + + //! Draws the overall CPU profiling window, defaults to the statistical view + void Draw(bool& keepDrawing); + + private: + static constexpr float RowHeight = 35.0f; + static constexpr int DefaultFramesToCollect = 50; + static constexpr float MediumFrameTimeLimit = 16.6f; // 60 fps + static constexpr float HighFrameTimeLimit = 33.3f; // 30 fps + + //! Draws the statistical view of the CPU profiling data. + void DrawStatisticsView(); + + //! Callback invoked when the "Load File" button is pressed in the file picker. + void LoadFile(); + + //! Draws the file picker window. + void DrawFilePicker(); + + //! Draws the CPU profiling visualizer. + void DrawVisualizer(); + + // Draw the shared header between the two windows. + void DrawCommonHeader(); + + // Draw the region statistics table in the order specified by the pointers in m_tableData. + void DrawTable(); + + // Sort the table by a given column, rearranges the pointers in m_tableData. + void SortTable(ImGuiTableSortSpecs* sortSpecs); + + // gather the latest timing statistics + void CacheCpuTimingStatistics(); + + // Get the profiling data from the last frame, only called when the profiler is not paused. + void CollectFrameData(); + + // Cull old data from internal storage, only called when profiler is not paused. + void CullFrameData(); + + // Draws a single block onto the timeline into the specified row + void DrawBlock(const TimeRegion& block, AZ::u64 targetRow); + + // Draw horizontal lines between threads in the timeline + void DrawThreadSeparator(AZ::u64 threadBoundary, AZ::u64 maxDepth); + + // Draw the "Thread XXXXX" label onto the viewport + void DrawThreadLabel(AZ::u64 baseRow, size_t threadId); + + // Draw the vertical lines separating frames in the timeline + void DrawFrameBoundaries(); + + // Draw the ruler with frame time labels + void DrawRuler(); + + // Draw the frame time histogram + void DrawFrameTimeHistogram(); + + // Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling + float ConvertTickToPixelSpace(AZStd::sys_time_t tick, AZStd::sys_time_t leftBound, AZStd::sys_time_t rightBound) const; + + AZStd::sys_time_t GetViewportTickWidth() const; + + // Gets the color for a block using the GroupRegionName as a key into the cache. + // Generates a random ImU32 if the block does not yet have a color. + ImU32 GetBlockColor(const TimeRegion& block); + + // System tick bus overrides + void OnSystemTick() override; + + // --- Visualizer Members --- + + int m_framesToCollect = DefaultFramesToCollect; + + // Tally of the number of saved profiling events so far + AZ::u64 m_savedRegionCount = 0; + + // Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects + AZStd::sys_time_t m_viewportStartTick; + AZStd::sys_time_t m_viewportEndTick; + + // Map to store each thread's TimeRegions, individual vectors are sorted by start tick + // note: we use size_t as a proxy for thread_id because native_thread_id_type differs differs from + // platform to platform, which causes problems when deserializing saved captures. + AZStd::unordered_map> m_savedData; + + // Region color cache + AZStd::unordered_map m_regionColorMap; + + // Tracks the frame boundaries + AZStd::vector m_frameEndTicks = { INT64_MIN }; + + // Filter for highlighting regions on the visualizer + ImGuiTextFilter m_visualizerHighlightFilter; + + // --- Tabular view members --- + + // ImGui filter used to filter TimedRegions. + ImGuiTextFilter m_timedRegionFilter; + + // Saves statistical view data organized by group name -> region name -> row data + GroupRegionMap m_groupRegionMap; + + // Saves pointers to objects in m_groupRegionMap, order reflects table ordering. + // Non-owning, will be cleared when m_groupRegionMap is cleared. + AZStd::vector m_tableData; + + // Pause cpu profiling. The profiler will show the statistics of the last frame before pause. + bool m_paused = false; + + // Export the profiling data from a single frame to a local file. + bool m_captureToFile = false; + + // Toggle between the normal statistical view and the visual profiling view. + bool m_enableVisualizer = false; + + // Last captured CPU timing statistics + AZStd::vector m_cpuTimingStatisticsWhenPause; + AZStd::sys_time_t m_frameToFrameTime{}; + + AZStd::string m_lastCapturedFilePath; + + bool m_showFilePicker = false; + + // Cached file paths to previous traces on disk, sorted with the most recent trace at the front. + AZStd::vector m_cachedCapturePaths; + + // Index into the file picker, used to determine which file to load when "Load File" is pressed. + int m_currentFileIndex = 0; + + + // --- Loading capture state --- + AZStd::unordered_set m_deserializedStringPool; + AZStd::unordered_set m_deserializedGroupRegionNamePool; + }; +} // namespace Profiler + +#endif // defined(IMGUI_ENABLED) diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp new file mode 100644 index 0000000000..38cfee2c26 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiModule.cpp @@ -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 + * + */ + +#include +#include + +#include +#include + +namespace Profiler +{ + class ProfilerImGuiModule + : public AZ::Module + { + public: + AZ_RTTI(ProfilerImGuiModule, "{5946991E-A96C-4E7A-A9B3-605E3C8EC3CB}", AZ::Module); + AZ_CLASS_ALLOCATOR(ProfilerImGuiModule, AZ::SystemAllocator, 0); + + ProfilerImGuiModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + ProfilerSystemComponent::CreateDescriptor(), + ProfilerImGuiSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + azrtti_typeid(), + }; + } + }; +}// namespace Profiler + +AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerImGuiModule) diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp new file mode 100644 index 0000000000..62d27a1806 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.cpp @@ -0,0 +1,116 @@ +/* + * 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 + +namespace Profiler +{ + static constexpr AZ::Crc32 profilerImGuiServiceCrc = AZ_CRC_CE("ProfilerImGuiService"); + + void ProfilerImGuiSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0); + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("ProfilerImGui", "Provides in-game visualization of the performance data gathered by the ProfilerSystemComponent") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void ProfilerImGuiSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(profilerImGuiServiceCrc); + } + + void ProfilerImGuiSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(profilerImGuiServiceCrc); + } + + void ProfilerImGuiSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void ProfilerImGuiSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + ProfilerImGuiSystemComponent::ProfilerImGuiSystemComponent() + { +#if defined(IMGUI_ENABLED) + if (ProfilerImGuiInterface::Get() == nullptr) + { + ProfilerImGuiInterface::Register(this); + } +#endif // defined(IMGUI_ENABLED) + } + + ProfilerImGuiSystemComponent::~ProfilerImGuiSystemComponent() + { +#if defined(IMGUI_ENABLED) + if (ProfilerImGuiInterface::Get() == this) + { + ProfilerImGuiInterface::Unregister(this); + } +#endif // defined(IMGUI_ENABLED) + } + + void ProfilerImGuiSystemComponent::Activate() + { +#if defined(IMGUI_ENABLED) + ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); +#endif // defined(IMGUI_ENABLED) + } + + void ProfilerImGuiSystemComponent::Deactivate() + { +#if defined(IMGUI_ENABLED) + ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect(); +#endif // defined(IMGUI_ENABLED) + } + +#if defined(IMGUI_ENABLED) + void ProfilerImGuiSystemComponent::ShowCpuProfilerWindow(bool& keepDrawing) + { + m_imguiCpuProfiler.Draw(keepDrawing); + } + + void ProfilerImGuiSystemComponent::OnImGuiUpdate() + { + if (m_showCpuProfiler) + { + ShowCpuProfilerWindow(m_showCpuProfiler); + } + } + + void ProfilerImGuiSystemComponent::OnImGuiMainMenuUpdate() + { + if (ImGui::BeginMenu("Profiler")) + { + if (ImGui::MenuItem("CPU", "", &m_showCpuProfiler)) + { + CpuProfiler::Get()->SetProfilerEnabled(m_showCpuProfiler); + } + ImGui::EndMenu(); + } + } +#endif // defined(IMGUI_ENABLED) +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h new file mode 100644 index 0000000000..351197f961 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerImGuiSystemComponent.h @@ -0,0 +1,65 @@ +/* + * 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 + +#if defined(IMGUI_ENABLED) +#include +#include +#endif // defined(IMGUI_ENABLED) + +namespace Profiler +{ + class ProfilerImGuiSystemComponent + : public AZ::Component +#if defined(IMGUI_ENABLED) + , public ProfilerImGuiRequests + , public ImGui::ImGuiUpdateListenerBus::Handler +#endif // defined(IMGUI_ENABLED) + { + public: + AZ_COMPONENT(ProfilerImGuiSystemComponent, "{E59A8A53-6784-4CCB-A8B5-9F91DA9BF1C5}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + ProfilerImGuiSystemComponent(); + ~ProfilerImGuiSystemComponent(); + + protected: + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + +#if defined(IMGUI_ENABLED) + // ProfilerImGuiRequests interface implementation + void ShowCpuProfilerWindow(bool& keepDrawing) override; + + // ImGuiUpdateListenerBus overrides + void OnImGuiUpdate() override; + void OnImGuiMainMenuUpdate() override; +#endif // defined(IMGUI_ENABLED) + + private: +#if defined(IMGUI_ENABLED) + ImGuiCpuProfiler m_imguiCpuProfiler; + bool m_showCpuProfiler{ false }; +#endif // defined(IMGUI_ENABLED) + }; + +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerModule.cpp b/Gems/Profiler/Code/Source/ProfilerModule.cpp new file mode 100644 index 0000000000..055c4cca4d --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerModule.cpp @@ -0,0 +1,46 @@ +/* + * 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 Profiler +{ + class ProfilerModule + : public AZ::Module + { + public: + AZ_RTTI(ProfilerModule, "{4A286414-B387-4D20-9A7E-2F792755B769}", AZ::Module); + AZ_CLASS_ALLOCATOR(ProfilerModule, AZ::SystemAllocator, 0); + + ProfilerModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + ProfilerSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } + }; +}// namespace Profiler + +AZ_DECLARE_MODULE_CLASS(Gem_Profiler, Profiler::ProfilerModule) diff --git a/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp new file mode 100644 index 0000000000..bc51ffd0a7 --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.cpp @@ -0,0 +1,284 @@ +/* + * 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 +#include + +namespace Profiler +{ + static constexpr AZ::Crc32 profilerServiceCrc = AZ_CRC_CE("ProfilerService"); + + struct DeplayedFunction + { + using func_type = AZStd::function; + + DeplayedFunction(int framesToDelay, func_type&& function) + : m_function(AZStd::move(function)) + , m_framesLeft(framesToDelay) + { + } + + void Run() + { + if (--m_framesLeft <= 0) + { + m_function(); + } + else + { + AZ::SystemTickBus::QueueFunction( + [](DeplayedFunction&& delayedFunc) + { + delayedFunc.Run(); + }, + AZStd::move(*this) + ); + } + } + + func_type m_function; + int m_framesLeft{ 0 }; + }; + + class ProfilerNotificationBusHandler final + : public ProfilerNotificationBus::Handler + , public AZ::BehaviorEBusHandler + { + public: + AZ_EBUS_BEHAVIOR_BINDER(ProfilerNotificationBusHandler, "{44161459-B816-4876-95A4-BA16DEC767D6}", AZ::SystemAllocator, + OnCaptureCpuProfilingStatisticsFinished + ); + + void OnCaptureCpuProfilingStatisticsFinished(bool result, const AZStd::string& info) override + { + Call(FN_OnCaptureCpuProfilingStatisticsFinished, result, info); + } + + static void Reflect(AZ::ReflectContext* context) + { + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->EBus("ProfilerNotificationBus") + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) + ->Attribute(AZ::Script::Attributes::Module, "profiler") + ->Handler(); + } + } + }; + + bool SerializeCpuProfilingData(const AZStd::ring_buffer& data, AZStd::string outputFilePath, bool wasEnabled) + { + AZ_TracePrintf("ProfilerSystemComponent", "Beginning serialization of %zu frames of profiling data\n", data.size()); + AZ::JsonSerializerSettings serializationSettings; + serializationSettings.m_keepDefaults = true; + + CpuProfilingStatisticsSerializer serializer(data); + + const auto saveResult = AZ::JsonSerializationUtils::SaveObjectToFile(&serializer, + outputFilePath, (CpuProfilingStatisticsSerializer*)nullptr, &serializationSettings); + + AZStd::string captureInfo = outputFilePath; + if (!saveResult.IsSuccess()) + { + captureInfo = AZStd::string::format("Failed to save Cpu Profiling Statistics data to file '%s'. Error: %s", + outputFilePath.c_str(), + saveResult.GetError().c_str()); + AZ_Warning("ProfilerSystemComponent", false, captureInfo.c_str()); + } + else + { + AZ_Printf("ProfilerSystemComponent", "Cpu profiling statistics was saved to file [%s]\n", outputFilePath.c_str()); + } + + // Disable the profiler again + if (!wasEnabled) + { + CpuProfiler::Get()->SetProfilerEnabled(false); + } + + // Notify listeners that the pass' PipelineStatistics queries capture has finished. + ProfilerNotificationBus::Broadcast(&ProfilerNotificationBus::Events::OnCaptureCpuProfilingStatisticsFinished, + saveResult.IsSuccess(), + captureInfo); + + return saveResult.IsSuccess(); + } + + void ProfilerSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (AZ::SerializeContext* serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0); + + if (AZ::EditContext* ec = serialize->GetEditContext()) + { + ec->Class("Profiler", "Provides a custom implementation of the AZ::Debug::Profiler interface for capturing performance data") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("System")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + + ProfilerNotificationBusHandler::Reflect(context); + } + } + + if (AZ::BehaviorContext* behaviorContext = azrtti_cast(context)) + { + behaviorContext->EBus("ProfilerRequestBus") + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation) + ->Attribute(AZ::Script::Attributes::Module, "profiler") + ->Event("CaptureCpuProfilingStatistics", &ProfilerRequestBus::Events::CaptureCpuProfilingStatistics); + + ProfilerNotificationBusHandler::Reflect(context); + } + + CpuProfilingStatisticsSerializer::Reflect(context); + } + + void ProfilerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(profilerServiceCrc); + } + + void ProfilerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(profilerServiceCrc); + } + + void ProfilerSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void ProfilerSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + ProfilerSystemComponent::ProfilerSystemComponent() + { + if (ProfilerInterface::Get() == nullptr) + { + ProfilerInterface::Register(this); + } + } + + ProfilerSystemComponent::~ProfilerSystemComponent() + { + if (ProfilerInterface::Get() == this) + { + ProfilerInterface::Unregister(this); + } + } + + void ProfilerSystemComponent::Activate() + { + ProfilerRequestBus::Handler::BusConnect(); + + m_cpuProfiler.Init(); + } + + void ProfilerSystemComponent::Deactivate() + { + m_cpuProfiler.Shutdown(); + + ProfilerRequestBus::Handler::BusDisconnect(); + + // Block deactivation until the IO thread has finished serializing the CPU data + if (m_cpuDataSerializationThread.joinable()) + { + m_cpuDataSerializationThread.join(); + } + } + + void ProfilerSystemComponent::SetProfilerEnabled(bool enabled) + { + m_cpuProfiler.SetProfilerEnabled(enabled); + } + + bool ProfilerSystemComponent::CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) + { + bool expected = false; + if (!m_cpuCaptureInProgress.compare_exchange_strong(expected, true)) + { + return false; + } + + // Start the cpu profiling + bool wasEnabled = m_cpuProfiler.IsProfilerEnabled(); + if (!wasEnabled) + { + m_cpuProfiler.SetProfilerEnabled(true); + } + + const int frameDelay = 5; // arbitrary number + DeplayedFunction delayedFunc(frameDelay, + [this, outputFilePath, wasEnabled]() + { + // Blocking call for a single frame of data, avoid thread overhead + AZStd::ring_buffer singleFrameData(1); + singleFrameData.push_back(m_cpuProfiler.GetTimeRegionMap()); + SerializeCpuProfilingData(singleFrameData, outputFilePath, wasEnabled); + m_cpuCaptureInProgress.store(false); + } + ); + delayedFunc.Run(); + + return true; + } + + bool ProfilerSystemComponent::BeginContinuousCpuProfilingCapture() + { + return m_cpuProfiler.BeginContinuousCapture(); + } + + bool ProfilerSystemComponent::EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) + { + bool expected = false; + if (!m_cpuDataSerializationInProgress.compare_exchange_strong(expected, true)) + { + AZ_TracePrintf( + "ProfilerSystemComponent", + "Cannot end a continuous capture - another serialization is currently in progress\n"); + return false; + } + + AZStd::ring_buffer captureResult; + const bool captureEnded = m_cpuProfiler.EndContinuousCapture(captureResult); + if (!captureEnded) + { + AZ_TracePrintf("ProfilerSystemComponent", "Could not end the continuous capture, is one in progress?\n"); + m_cpuDataSerializationInProgress.store(false); + return false; + } + + // cpuProfilingData could be 1GB+ once saved, so use an IO thread to write it to disk. + auto threadIoFunction = + [data = AZStd::move(captureResult), filePath = AZStd::string(outputFilePath), &flag = m_cpuDataSerializationInProgress]() + { + SerializeCpuProfilingData(data, filePath, true); + flag.store(false); + }; + + // If the thread object already exists (ex. we have already serialized data), join. This will not block since + // m_cpuDataSerializationInProgress was false, meaning the IO thread has already completed execution. + if (m_cpuDataSerializationThread.joinable()) + { + m_cpuDataSerializationThread.join(); + } + + auto thread = AZStd::thread(threadIoFunction); + m_cpuDataSerializationThread = AZStd::move(thread); + + return true; + } +} // namespace Profiler diff --git a/Gems/Profiler/Code/Source/ProfilerSystemComponent.h b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h new file mode 100644 index 0000000000..76121be04f --- /dev/null +++ b/Gems/Profiler/Code/Source/ProfilerSystemComponent.h @@ -0,0 +1,56 @@ +/* + * 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 + +namespace Profiler +{ + class ProfilerSystemComponent + : public AZ::Component + , protected ProfilerRequestBus::Handler + { + public: + AZ_COMPONENT(ProfilerSystemComponent, "{3f52c1d7-d920-4781-8ed7-88077ec4f305}"); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + ProfilerSystemComponent(); + ~ProfilerSystemComponent(); + + protected: + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + + // ProfilerRequestBus interface implementation + void SetProfilerEnabled(bool enabled) override; + bool CaptureCpuProfilingStatistics(const AZStd::string& outputFilePath) override; + bool BeginContinuousCpuProfilingCapture() override; + bool EndContinuousCpuProfilingCapture(const AZStd::string& outputFilePath) override; + + + AZStd::thread m_cpuDataSerializationThread; + AZStd::atomic_bool m_cpuDataSerializationInProgress{ false }; + + AZStd::atomic_bool m_cpuCaptureInProgress{ false }; + + CpuProfilerImpl m_cpuProfiler; + }; + +} // namespace Profiler diff --git a/Gems/Profiler/Code/profiler_files.cmake b/Gems/Profiler/Code/profiler_files.cmake new file mode 100644 index 0000000000..51ceb00139 --- /dev/null +++ b/Gems/Profiler/Code/profiler_files.cmake @@ -0,0 +1,17 @@ +# +# 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 + Include/Profiler/ProfilerBus.h + Include/Profiler/ProfilerImGuiBus.h + Source/CpuProfiler.h + Source/CpuProfilerImpl.cpp + Source/CpuProfilerImpl.h + Source/ProfilerSystemComponent.cpp + Source/ProfilerSystemComponent.h +) diff --git a/Gems/Profiler/Code/profiler_imgui_shared_files.cmake b/Gems/Profiler/Code/profiler_imgui_shared_files.cmake new file mode 100644 index 0000000000..216f6ceeed --- /dev/null +++ b/Gems/Profiler/Code/profiler_imgui_shared_files.cmake @@ -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 +# +# + +set(FILES + Source/ImGuiCpuProfiler.cpp + Source/ImGuiCpuProfiler.h + Source/ProfilerImGuiModule.cpp + Source/ProfilerImGuiSystemComponent.cpp + Source/ProfilerImGuiSystemComponent.h +) diff --git a/Gems/Profiler/Code/profiler_shared_files.cmake b/Gems/Profiler/Code/profiler_shared_files.cmake new file mode 100644 index 0000000000..b8ee476e23 --- /dev/null +++ b/Gems/Profiler/Code/profiler_shared_files.cmake @@ -0,0 +1,11 @@ +# +# 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 + Source/ProfilerModule.cpp +) diff --git a/Gems/Profiler/gem.json b/Gems/Profiler/gem.json new file mode 100644 index 0000000000..2f121d7618 --- /dev/null +++ b/Gems/Profiler/gem.json @@ -0,0 +1,19 @@ +{ + "gem_name": "Profiler", + "display_name": "Profiler", + "license": "Apache-2.0 Or MIT", + "origin": "Open 3D Engine - o3de.org", + "type": "Code", + "summary": "A collection of utilities for capturing performance data", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "Profiler" + ], + "icon_path": "preview.png", + "requirements": "", + "dependencies": [ + "ImGui" + ] +} diff --git a/Gems/Profiler/preview.png b/Gems/Profiler/preview.png new file mode 100644 index 0000000000..0f393ac886 --- /dev/null +++ b/Gems/Profiler/preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ac9dd09bde78f389e3725ac49d61eff109857e004840bc0bc3881739df9618d +size 2217 diff --git a/Templates/DefaultProject/Template/EngineFinder.cmake b/Templates/DefaultProject/Template/EngineFinder.cmake index 1bb4950e7d..98ad61bae8 100644 --- a/Templates/DefaultProject/Template/EngineFinder.cmake +++ b/Templates/DefaultProject/Template/EngineFinder.cmake @@ -18,7 +18,7 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_CURRENT_L string(JSON LY_ENGINE_NAME_TO_USE ERROR_VARIABLE json_error GET ${project_json} engine) if(json_error) - message(FATAL_ERROR "Unable to read key 'engine' from 'project.json', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engine' from 'project.json'\nError: ${json_error}") endif() if(CMAKE_MODULE_PATH) @@ -27,7 +27,7 @@ if(CMAKE_MODULE_PATH) file(READ ${module_path}/../engine.json engine_json) string(JSON engine_name ERROR_VARIABLE json_error GET ${engine_json} engine_name) if(json_error) - message(FATAL_ERROR "Unable to read key 'engine_name' from 'engine.json', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engine_name' from 'engine.json'\nError: ${json_error}") endif() if(LY_ENGINE_NAME_TO_USE STREQUAL engine_name) return() # Engine being forced through CMAKE_MODULE_PATH @@ -42,6 +42,11 @@ else() set(manifest_path $ENV{HOME}/.o3de/o3de_manifest.json) # Unix endif() +set(registration_error [=[ +Engine registration is required before configuring a project. +Run 'scripts/o3de register --this-engine' from the engine root. +]=]) + # Read the ~/.o3de/o3de_manifest.json file and look through the 'engines_path' object. # Find a key that matches LY_ENGINE_NAME_TO_USE and use that as the engine path. if(EXISTS ${manifest_path}) @@ -50,36 +55,38 @@ if(EXISTS ${manifest_path}) string(JSON engines_path_count ERROR_VARIABLE json_error LENGTH ${manifest_json} engines_path) if(json_error) - message(FATAL_ERROR "Unable to read key 'engines_path' from '${manifest_path}', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engines_path' from '${manifest_path}'\nError: ${json_error}\n${registration_error}") endif() string(JSON engines_path_type ERROR_VARIABLE json_error TYPE ${manifest_json} engines_path) if(json_error OR NOT ${engines_path_type} STREQUAL "OBJECT") - message(FATAL_ERROR "Type of 'engines_path' in '${manifest_path}' is not a JSON Object, error: ${json_error}") + message(FATAL_ERROR "Type of 'engines_path' in '${manifest_path}' is not a JSON Object\nError: ${json_error}") endif() math(EXPR engines_path_count "${engines_path_count}-1") foreach(engine_path_index RANGE ${engines_path_count}) string(JSON engine_name ERROR_VARIABLE json_error MEMBER ${manifest_json} engines_path ${engine_path_index}) if(json_error) - message(FATAL_ERROR "Unable to read 'engines_path/${engine_path_index}' from '${manifest_path}', error: ${json_error}") + message(FATAL_ERROR "Unable to read 'engines_path/${engine_path_index}' from '${manifest_path}'\nError: ${json_error}") endif() if(LY_ENGINE_NAME_TO_USE STREQUAL engine_name) string(JSON engine_path ERROR_VARIABLE json_error GET ${manifest_json} engines_path ${engine_name}) if(json_error) - message(FATAL_ERROR "Unable to read value from 'engines_path/${engine_name}', error: ${json_error}") + message(FATAL_ERROR "Unable to read value from 'engines_path/${engine_name}'\nError: ${json_error}") endif() if(engine_path) list(APPEND CMAKE_MODULE_PATH "${engine_path}/cmake") - break() + return() endif() endif() endforeach() + + message(FATAL_ERROR "The project.json uses engine name '${LY_ENGINE_NAME_TO_USE}' but no engine with that name has been registered.\n${registration_error}") else() # If the user is passing CMAKE_MODULE_PATH we assume thats where we will find the engine if(NOT CMAKE_MODULE_PATH) - message(FATAL_ERROR "Engine registration is required before configuring a project. Please register an engine by running 'scripts/o3de register --this-engine'") + message(FATAL_ERROR "O3DE Manifest file not found.\n${registration_error}") endif() endif() diff --git a/Templates/MinimalProject/Template/EngineFinder.cmake b/Templates/MinimalProject/Template/EngineFinder.cmake index 1bb4950e7d..98ad61bae8 100644 --- a/Templates/MinimalProject/Template/EngineFinder.cmake +++ b/Templates/MinimalProject/Template/EngineFinder.cmake @@ -18,7 +18,7 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CMAKE_CURRENT_L string(JSON LY_ENGINE_NAME_TO_USE ERROR_VARIABLE json_error GET ${project_json} engine) if(json_error) - message(FATAL_ERROR "Unable to read key 'engine' from 'project.json', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engine' from 'project.json'\nError: ${json_error}") endif() if(CMAKE_MODULE_PATH) @@ -27,7 +27,7 @@ if(CMAKE_MODULE_PATH) file(READ ${module_path}/../engine.json engine_json) string(JSON engine_name ERROR_VARIABLE json_error GET ${engine_json} engine_name) if(json_error) - message(FATAL_ERROR "Unable to read key 'engine_name' from 'engine.json', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engine_name' from 'engine.json'\nError: ${json_error}") endif() if(LY_ENGINE_NAME_TO_USE STREQUAL engine_name) return() # Engine being forced through CMAKE_MODULE_PATH @@ -42,6 +42,11 @@ else() set(manifest_path $ENV{HOME}/.o3de/o3de_manifest.json) # Unix endif() +set(registration_error [=[ +Engine registration is required before configuring a project. +Run 'scripts/o3de register --this-engine' from the engine root. +]=]) + # Read the ~/.o3de/o3de_manifest.json file and look through the 'engines_path' object. # Find a key that matches LY_ENGINE_NAME_TO_USE and use that as the engine path. if(EXISTS ${manifest_path}) @@ -50,36 +55,38 @@ if(EXISTS ${manifest_path}) string(JSON engines_path_count ERROR_VARIABLE json_error LENGTH ${manifest_json} engines_path) if(json_error) - message(FATAL_ERROR "Unable to read key 'engines_path' from '${manifest_path}', error: ${json_error}") + message(FATAL_ERROR "Unable to read key 'engines_path' from '${manifest_path}'\nError: ${json_error}\n${registration_error}") endif() string(JSON engines_path_type ERROR_VARIABLE json_error TYPE ${manifest_json} engines_path) if(json_error OR NOT ${engines_path_type} STREQUAL "OBJECT") - message(FATAL_ERROR "Type of 'engines_path' in '${manifest_path}' is not a JSON Object, error: ${json_error}") + message(FATAL_ERROR "Type of 'engines_path' in '${manifest_path}' is not a JSON Object\nError: ${json_error}") endif() math(EXPR engines_path_count "${engines_path_count}-1") foreach(engine_path_index RANGE ${engines_path_count}) string(JSON engine_name ERROR_VARIABLE json_error MEMBER ${manifest_json} engines_path ${engine_path_index}) if(json_error) - message(FATAL_ERROR "Unable to read 'engines_path/${engine_path_index}' from '${manifest_path}', error: ${json_error}") + message(FATAL_ERROR "Unable to read 'engines_path/${engine_path_index}' from '${manifest_path}'\nError: ${json_error}") endif() if(LY_ENGINE_NAME_TO_USE STREQUAL engine_name) string(JSON engine_path ERROR_VARIABLE json_error GET ${manifest_json} engines_path ${engine_name}) if(json_error) - message(FATAL_ERROR "Unable to read value from 'engines_path/${engine_name}', error: ${json_error}") + message(FATAL_ERROR "Unable to read value from 'engines_path/${engine_name}'\nError: ${json_error}") endif() if(engine_path) list(APPEND CMAKE_MODULE_PATH "${engine_path}/cmake") - break() + return() endif() endif() endforeach() + + message(FATAL_ERROR "The project.json uses engine name '${LY_ENGINE_NAME_TO_USE}' but no engine with that name has been registered.\n${registration_error}") else() # If the user is passing CMAKE_MODULE_PATH we assume thats where we will find the engine if(NOT CMAKE_MODULE_PATH) - message(FATAL_ERROR "Engine registration is required before configuring a project. Please register an engine by running 'scripts/o3de register --this-engine'") + message(FATAL_ERROR "O3DE Manifest file not found.\n${registration_error}") endif() endif() diff --git a/cmake/Install.cmake b/cmake/Install.cmake index adcd28fa37..3b74e6c654 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -6,10 +6,7 @@ # # -ly_set(LY_INSTALL_ENABLED TRUE) -if(INSTALLED_ENGINE) - ly_set(LY_INSTALL_ENABLED FALSE) -endif() +set(LY_INSTALL_ENABLED TRUE CACHE BOOL "Indicates if the install process is enabled") if(LY_INSTALL_ENABLED) ly_get_absolute_pal_filename(pal_dir ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Platform/${PAL_PLATFORM_NAME}) diff --git a/cmake/LYWrappers.cmake b/cmake/LYWrappers.cmake index d388b03b75..fb3d420c26 100644 --- a/cmake/LYWrappers.cmake +++ b/cmake/LYWrappers.cmake @@ -315,13 +315,16 @@ function(ly_add_target) # Store the target so we can walk through all of them in LocationDependencies.cmake set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGETS ${interface_name}) - # Store the aliased target into a DIRECTORY property - set_property(DIRECTORY APPEND PROPERTY LY_DIRECTORY_TARGETS ${interface_name}) - # Store the directory path in a GLOBAL property so that it can be accessed - # in the layout install logic. Skip if the directory has already been added - get_property(ly_all_target_directories GLOBAL PROPERTY LY_ALL_TARGET_DIRECTORIES) - if(NOT CMAKE_CURRENT_SOURCE_DIR IN_LIST ly_all_target_directories) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGET_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}) + if(NOT ly_add_target_IMPORTED) + # Store the aliased target into a DIRECTORY property + set_property(DIRECTORY APPEND PROPERTY LY_DIRECTORY_TARGETS ${interface_name}) + + # Store the directory path in a GLOBAL property so that it can be accessed + # in the layout install logic. Skip if the directory has already been added + get_property(ly_all_target_directories GLOBAL PROPERTY LY_ALL_TARGET_DIRECTORIES) + if(NOT CMAKE_CURRENT_SOURCE_DIR IN_LIST ly_all_target_directories) + set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGET_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}) + endif() endif() # Custom commands need to be declared in the same folder as the target that they use. diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index 8fb2effe29..c453fadd2e 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -385,7 +385,6 @@ function(ly_setup_cmake_install) COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} PATTERN "__pycache__" EXCLUDE PATTERN "Findo3de.cmake" EXCLUDE - PATTERN "ConfigurationTypes.cmake" EXCLUDE REGEX "3rdParty/Platform\/.*\/BuiltInPackages_.*\.cmake" EXCLUDE ) # Connect configuration types diff --git a/engine.json b/engine.json index 6646c3bec3..14deffecb8 100644 --- a/engine.json +++ b/engine.json @@ -58,6 +58,7 @@ "Gems/Prefab", "Gems/Presence", "Gems/PrimitiveAssets", + "Gems/Profiler", "Gems/PythonAssetBuilder", "Gems/QtForPython", "Gems/SaveData", diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index ffca69a14f..1388366661 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -416,16 +416,18 @@ def ExportTestResults(Map options, String platform, String type, String workspac } } -def ExportTestScreenshots(Map options, String workspace, String platformName, String jobName, Map params) { +def ExportTestScreenshots(Map options, String branchName, String platformName, String jobName, String workspace, Map params) { catchError(message: "Error exporting test screenshots (this won't fail the build)", buildResult: 'SUCCESS', stageResult: 'FAILURE') { - def screenshotsFolder = '${workspace}/${ENGINE_REPOSITORY_NAME}/AutomatedTesting/user/PythonTests/Automated/Screenshots' - def s3Uploader = '${workspace}/${ENGINE_REPOSITORY_NAME}/scripts/build/tools/upload_to_s3.py' - def command = '${options.PYTHON_DIR}/python.cmd -u ${s3Uploader} --base_dir ${screenshotsFolder} ' + - '--file_regex "(.*zip$)" --bucket ${env.TEST_SCREENSHOT_BUCKET} ' + - '--search_subdirectories True --key_prefix ${branchName}_${env.BUILD_NUMBER}' + - '--extra-args {"ACL": "bucket-owner-full-control"}' - bat label: "Uploading test screenshots for ${jobName}", - script: command + dir("${workspace}/${ENGINE_REPOSITORY_NAME}") { + def screenshotsFolder = "AutomatedTesting/user/PythonTests/Automated/Screenshots" + def s3Uploader = "scripts/build/tools/upload_to_s3.py" + def command = "${options.PYTHON_DIR}/python.cmd -u ${s3Uploader} --base_dir ${screenshotsFolder} " + + '--file_regex \\"(.*zip\$)\\" ' + + "--bucket ${env.TEST_SCREENSHOT_BUCKET} " + + "--search_subdirectories True --key_prefix ${branchName}_${env.BUILD_NUMBER} " + + '--extra_args {\\"ACL\\":\\"bucket-owner-full-control\\"}' + palSh(command, "Uploading test screenshots for ${jobName}") + } } } @@ -484,10 +486,10 @@ def CreateExportTestResultsStage(Map pipelineConfig, String platformName, String } } -def CreateExportTestScreenshotsStage(Map pipelineConfig, String platformName, String jobName, Map environmentVars, Map params) { +def CreateExportTestScreenshotsStage(Map pipelineConfig, String branchName, String platformName, String jobName, Map environmentVars, Map params) { return { stage("${jobName}_screenshots") { - ExportTestScreenshots(pipelineConfig, platformName, jobName, environmentVars['WORKSPACE'], params) + ExportTestScreenshots(pipelineConfig, branchName, platformName, jobName, environmentVars['WORKSPACE'], params) } } } @@ -553,10 +555,10 @@ def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVar CreateTestMetricsStage(pipelineConfig, branchName, envVars, build_job_name, output_directory, configuration).call() } if (params && params.containsKey('TEST_RESULTS') && params.TEST_RESULTS == 'True') { - CreateExportTestResultsStage(pipelineConfig, platform.key, build_job_name, envVars, params).call() + CreateExportTestResultsStage(pipelineConfig, platform.key, build_job_name, envVars, params).call() } if (params && params.containsKey('TEST_SCREENSHOTS') && params.TEST_SCREENSHOTS == 'True' && currentResult == 'FAILURE') { - CreateExportTestScreenshotsStage(pipelineConfig, platform.key, build_job_name, envVars, params).call() + CreateExportTestScreenshotsStage(pipelineConfig, branchName, platform.key, build_job_name, envVars, params).call() } CreateTeardownStage(envVars).call() }