diff --git a/.gitattributes b/.gitattributes index d7f9d36c50..2037909dbd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -115,7 +115,6 @@ *.wav filter=lfs diff=lfs merge=lfs -text *.webm filter=lfs diff=lfs merge=lfs -text *.wem filter=lfs diff=lfs merge=lfs -text -*.wxs filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.tbscene filter=lfs diff=lfs merge=lfs -text *.spp filter=lfs diff=lfs merge=lfs -text diff --git a/AutomatedTesting/Config/aws_resource_mappings.json b/AutomatedTesting/Config/aws_resource_mappings.json new file mode 100644 index 0000000000..03a611b749 --- /dev/null +++ b/AutomatedTesting/Config/aws_resource_mappings.json @@ -0,0 +1,6 @@ +{ + "AWSResourceMappings": {}, + "AccountId": "", + "Region": "us-west-2", + "Version": "1.0.0" +} \ No newline at end of file diff --git a/AutomatedTesting/Gem/Code/runtime_dependencies.cmake b/AutomatedTesting/Gem/Code/runtime_dependencies.cmake index 280c25bcf7..33c2bf8d5f 100644 --- a/AutomatedTesting/Gem/Code/runtime_dependencies.cmake +++ b/AutomatedTesting/Gem/Code/runtime_dependencies.cmake @@ -45,4 +45,7 @@ set(GEM_DEPENDENCIES Gem::Atom_AtomBridge Gem::NvCloth Gem::Blast + Gem::AWSCore + Gem::AWSClientAuth + Gem::AWSMetrics ) diff --git a/AutomatedTesting/Gem/Code/tool_dependencies.cmake b/AutomatedTesting/Gem/Code/tool_dependencies.cmake index fc50707c12..d4a49bfad5 100644 --- a/AutomatedTesting/Gem/Code/tool_dependencies.cmake +++ b/AutomatedTesting/Gem/Code/tool_dependencies.cmake @@ -55,4 +55,7 @@ set(GEM_DEPENDENCIES Gem::Atom_AtomBridge.Editor Gem::NvCloth.Editor Gem::Blast.Editor + Gem::AWSCore.Editor + Gem::AWSClientAuth + Gem::AWSMetrics ) diff --git a/AutomatedTesting/Gem/PythonTests/AWS/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/AWS/CMakeLists.txt new file mode 100644 index 0000000000..b406ea77de --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/CMakeLists.txt @@ -0,0 +1,31 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +################################################################################ +# AWS Automated Tests +# Runs AWS Gems automation tests. +################################################################################ + +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) + # Enable after installing NodeJS and CDK on jenkins Windows AMI. + #ly_add_pytest( + # NAME AutomatedTesting::AWSTests + # TEST_SUITE periodic + # TEST_SERIAL + # PATH ${CMAKE_CURRENT_LIST_DIR}/AWS/${PAL_PLATFORM_NAME}/ + # RUNTIME_DEPENDENCIES + # Legacy::Editor + # AZ::AssetProcessor + # AutomatedTesting.Assets + # COMPONENT + # AWS + #) +endif() diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py new file mode 100644 index 0000000000..455b3f94cb --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py @@ -0,0 +1,155 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import os +import pytest +import boto3 + +import ly_test_tools.environment.process_utils as process_utils +from typing import List + + +class Cdk: + """ + Cdk class that provides methods to run cdk application commands. + Expects system to have NodeJS, AWS CLI and CDK installed globally and have their paths setup as env variables. + """ + def __init__(self, cdk_path: str, project: str, account_id: str, + workspace: pytest.fixture, session: boto3.session.Session): + """ + :param cdk_path: Path where cdk app.py is stored. + :param project: Project name used for cdk project name env variable. + :param account_id: AWS account id to use with cdk application. + :param workspace: ly_test_tools workspace fixture. + """ + self._cdk_env = os.environ.copy() + self._cdk_env['O3DE_AWS_PROJECT_NAME'] = project + self._cdk_env['O3DE_AWS_DEPLOY_REGION'] = session.region_name + self._cdk_env['O3DE_AWS_DEPLOY_ACCOUNT'] = account_id + self._cdk_env['PATH'] = f'{workspace.paths.engine_root()}\\python;' + self._cdk_env['PATH'] + + credentials = session.get_credentials().get_frozen_credentials() + self._cdk_env['AWS_ACCESS_KEY_ID'] = credentials.access_key + self._cdk_env['AWS_SECRET_ACCESS_KEY'] = credentials.secret_key + self._cdk_env['AWS_SESSION_TOKEN'] = credentials.token + self._stacks = [] + self._cdk_path = cdk_path + + output = process_utils.check_output( + 'python -m pip install -r requirements.txt', + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + + def list(self) -> List[str]: + """ + lists cdk stack names + :return List of cdk stack names + """ + + if not self._cdk_path: + return [] + + list_cdk_application_cmd = ['cdk', 'list'] + output = process_utils.check_output( + list_cdk_application_cmd, + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + + return output.splitlines() + + def synthesize(self) -> None: + """ + Synthesizes all cdk stacks + """ + if not self._cdk_path: + return + + list_cdk_application_cmd = ['cdk', 'synth'] + + process_utils.check_output( + list_cdk_application_cmd, + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + + def deploy(self, context_variable: str = '') -> List[str]: + """ + Deploys all the CDK stacks. + :param context_variable: Context variable for enabling optional features. + :return List of deployed stack arns. + """ + if not self._cdk_path: + return [] + + deploy_cdk_application_cmd = ['cdk', 'deploy', '--require-approval', 'never'] + if context_variable: + deploy_cdk_application_cmd.extend(['-c', f'{context_variable}']) + + output = process_utils.check_output( + deploy_cdk_application_cmd, + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + + stacks = [] + for line in output.splitlines(): + line_sections = line.split('/') + assert len(line_sections), 3 + stacks.append(line.split('/')[-2]) + + return stacks + + def destroy(self) -> None: + """ + Destroys the cdk application. + """ + destroy_cdk_application_cmd = ['cdk', 'destroy', '-f'] + process_utils.check_output( + destroy_cdk_application_cmd, + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + + self._stacks = [] + self._cdk_path = '' + + +@pytest.fixture(scope='function') +def cdk( + request: pytest.fixture, + project: str, + feature_name: str, + workspace: pytest.fixture, + aws_utils: pytest.fixture, + destroy_stacks_on_teardown: bool = True) -> Cdk: + """ + Fixture for setting up a Cdk + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param project: Project name used for cdk project name env variable. + :param feature_name: Feature gem name to expect cdk folder in. + :param workspace: ly_test_tools workspace fixture. + :param aws_utils: aws_utils fixture. + :param destroy_stacks_on_teardown: option to control calling destroy ot the end of test. + :return Cdk class object. + """ + + cdk_path = f'{workspace.paths.engine_root()}/Gems/{feature_name}/cdk' + cdk_obj = Cdk(cdk_path, project, aws_utils.assume_account_id(), workspace, aws_utils.assume_session()) + + def teardown(): + if destroy_stacks_on_teardown: + cdk_obj.destroy() + request.addfinalizer(teardown) + + return cdk_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py new file mode 100644 index 0000000000..5997701870 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py @@ -0,0 +1,78 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import pytest +import os +import logging +import ly_test_tools.log.log_monitor + +from AWS.Windows.resource_mappings.resource_mappings import resource_mappings +from AWS.Windows.cdk.cdk import cdk +from AWS.common.aws_utils import aws_utils +from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor as asset_processor + +AWS_PROJECT_NAME = 'AWS-AutomationTest' +AWS_CLIENT_AUTH_FEATURE_NAME = 'AWSClientAuth' +AWS_CLIENT_AUTH_DEFAULT_PROFILE_NAME = 'default' + +GAME_LOG_NAME = 'Game.log' + +logger = logging.getLogger(__name__) + + +@pytest.mark.SUITE_periodic +@pytest.mark.usefixtures('automatic_process_killer') +@pytest.mark.usefixtures('asset_processor') +@pytest.mark.usefixtures('workspace') +@pytest.mark.parametrize('project', ['AutomatedTesting']) +@pytest.mark.parametrize('level', ['AWS/ClientAuth']) +@pytest.mark.usefixtures('cdk') +@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME]) +@pytest.mark.usefixtures('resource_mappings') +@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json']) +@pytest.mark.usefixtures('aws_utils') +@pytest.mark.parametrize('region_name', ['us-west-2']) +@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests']) +@pytest.mark.parametrize('session_name', ['o3de-Automation-session']) +class TestAWSClientAuthAnonymousCredentials(object): + """ + Test class to verify AWS Cognito Identity pool anonymous authorization. + """ + + def test_anonymous_credentials(self, + level: str, + launcher: pytest.fixture, + cdk: pytest.fixture, + resource_mappings: pytest.fixture, + workspace: pytest.fixture, + asset_processor: pytest.fixture + ): + """ + Setup: Deploys cdk and updates resource mapping file. + Tests: Getting AWS credentials for no signed in user. + Verification: Log monitor looks for success credentials log. + """ + logger.info(f'Cdk stack names:\n{cdk.list()}') + stacks = cdk.deploy() + resource_mappings.populate_output_keys(stacks) + asset_processor.start() + asset_processor.wait_for_idle() + + file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), GAME_LOG_NAME) + log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor) + + launcher.args = ['+LoadLevel', level] + + with launcher.start(launch_ap=False): + result = log_monitor.monitor_log_for_lines( + expected_lines=['(Script) - Success anonymous credentials'], + unexpected_lines=['(Script) - Fail anonymous credentials'], + halt_on_unexpected=True, + ) + assert result, 'Anonymous credentials fetched successfully.' diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/__init__.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/__init__.py new file mode 100644 index 0000000000..6ed3dc4bda --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/resource_mappings.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/resource_mappings.py new file mode 100644 index 0000000000..c8d8cff828 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/resource_mappings/resource_mappings.py @@ -0,0 +1,137 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import os +import pytest +import json + +AWS_RESOURCE_MAPPINGS_KEY = 'AWSResourceMappings' +AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY = 'AccountId' +AWS_RESOURCE_MAPPINGS_REGION_KEY = 'Region' + + +class ResourceMappings: + """ + ResourceMappings class that handles writing Cloud formation outputs to resource mappings json file in a project. + """ + + def __init__(self, file_path: str, region: str, feature_name: str, account_id: str, workspace: pytest.fixture, + cloud_formation_client): + """ + :param file_path: Path for the resource mapping file. + :param region: Region value for the resource mapping file. + :param feature_name: Feature gem name to use to append name to mappings key. + :param account_id: AWS account id value for the resource mapping file. + :param workspace: ly_test_tools workspace fixture. + :param cloud_formation_client: AWS cloud formation client. + """ + self._cdk_env = os.environ.copy() + self._cdk_env['PATH'] = f'{workspace.paths.engine_root()}\\python;' + self._cdk_env['PATH'] + self._resource_mapping_file_path = file_path + self._region = region + self._feature_name = feature_name + self._account_id = account_id + + assert os.path.exists(self._resource_mapping_file_path), \ + f'Invalid resource mapping file path {self._resource_mapping_file_path}' + self._client = cloud_formation_client + + def populate_output_keys(self, stacks=[]) -> None: + """ + Calls describe stacks on cloud formation service and persists outputs to resource mappings file. + :param stacks List of stack arns to describe and populate resource mappings with. + """ + for stack_name in stacks: + response = self._client.describe_stacks( + StackName=stack_name + ) + stacks = response.get('Stacks', []) + assert len(stacks) == 1, f'{stack_name} is invalid.' + + self.__write_resource_mappings(stacks[0].get('Outputs', [])) + + def __write_resource_mappings(self, outputs, append_feature_name = True) -> None: + with open(self._resource_mapping_file_path) as file_content: + resource_mappings = json.load(file_content) + + resource_mappings[AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY] = self._account_id + resource_mappings[AWS_RESOURCE_MAPPINGS_REGION_KEY] = self._region + + # Append new mappings. + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = resource_mappings.get(AWS_RESOURCE_MAPPINGS_KEY, {}) + + for output in outputs: + if append_feature_name: + resource_key = f'{self._feature_name}.{output.get("OutputKey", "InvalidKey")}' + else: + resource_key = output.get("OutputKey", "InvalidKey") + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key] = resource_mappings[ + AWS_RESOURCE_MAPPINGS_KEY].get(resource_key, {}) + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key]['Type'] = 'AutomationTestType' + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key]['Name/ID'] = output.get('OutputValue', + 'InvalidId') + + with open(self._resource_mapping_file_path, 'w') as file_content: + json.dump(resource_mappings, file_content, indent=4) + + def clear_output_keys(self) -> None: + """ + Clears values of all resource mapping keys. Sets region to default to us-west-2 + """ + with open(self._resource_mapping_file_path) as file_content: + resource_mappings = json.load(file_content) + + resource_mappings[AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY] = '' + resource_mappings[AWS_RESOURCE_MAPPINGS_REGION_KEY] = 'us-west-2' + + # Append new mappings. + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = resource_mappings.get(AWS_RESOURCE_MAPPINGS_KEY, {}) + resource_mappings[AWS_RESOURCE_MAPPINGS_KEY] = {} + + with open(self._resource_mapping_file_path, 'w') as file_content: + json.dump(resource_mappings, file_content, indent=4) + + self._resource_mapping_file_path = '' + self._region = '' + self._client = None + + +@pytest.fixture(scope='function') +def resource_mappings( + request: pytest.fixture, + project: str, + feature_name: str, + resource_mappings_filename: str, + workspace: pytest.fixture, + aws_utils: pytest.fixture) -> ResourceMappings: + """ + Fixture for setting up resource mappings file. + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param project: Project to find resource mapping file. + :param feature_name: AWS Gem name that is prepended to resource mapping keys. + :param resource_mappings_filename: Name of resource mapping file. + :param workspace: ly_test_tools workspace fixture. + :param aws_utils: AWS utils fixture. + :return: ResourceMappings class object. + """ + + path = f'{workspace.paths.engine_root()}\\{project}\\Config\\{resource_mappings_filename}' + resource_mappings_obj = ResourceMappings(path, aws_utils.assume_session().region_name, feature_name, + aws_utils.assume_account_id(), workspace, + aws_utils.client('cloudformation')) + + def teardown(): + resource_mappings_obj.clear_output_keys() + + request.addfinalizer(teardown) + + return resource_mappings_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/__init__.py b/AutomatedTesting/Gem/PythonTests/AWS/__init__.py new file mode 100644 index 0000000000..8caef52682 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/__init__.py @@ -0,0 +1,11 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + diff --git a/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py new file mode 100644 index 0000000000..7a15ba0abe --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py @@ -0,0 +1,82 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import boto3 +import pytest +import logging + +logger = logging.getLogger(__name__) + + +class AwsUtils: + + def __init__(self, arn: str, session_name: str, region_name: str): + local_session = boto3.Session(profile_name='default') + local_sts_client = local_session.client('sts') + self._local_account_id = local_sts_client.get_caller_identity()["Account"] + logger.info(f'Local Account Id: {self._local_account_id}') + + response = local_sts_client.assume_role(RoleArn=arn, RoleSessionName=session_name) + + self._assume_session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'], + aws_secret_access_key=response['Credentials']['SecretAccessKey'], + aws_session_token=response['Credentials']['SessionToken'], + region_name=region_name) + + assume_sts_client = self._assume_session.client('sts') + assume_account_id = assume_sts_client.get_caller_identity()["Account"] + logger.info(f'Assume Account Id: {assume_account_id}') + self._assume_account_id = assume_account_id + + def client(self, service: str): + """ + Get the client for a specific AWS service from configured session + :return: Client for the AWS service. + """ + return self._assume_session.client(service) + + def assume_session(self): + return self._assume_session + + def local_account_id(self): + return self._local_account_id + + def assume_account_id(self): + return self._assume_account_id + + def destroy(self) -> None: + """ + clears stored session + """ + self._assume_session = None + + +@pytest.fixture(scope='function') +def aws_utils( + request: pytest.fixture, + assume_role_arn: str, + session_name: str, + region_name: str): + """ + Fixture for setting up a Cdk + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param assume_role_arn: Role used to fetch temporary aws credentials, configure service clients with obtained credentials. + :param session_name: Session name to set. + :param region_name: AWS account region to set for session. + :return AWSUtils class object. + """ + aws_utils_obj = AwsUtils(assume_role_arn, session_name, region_name) + + def teardown(): + aws_utils_obj.destroy() + + request.addfinalizer(teardown) + + return aws_utils_obj diff --git a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt index d6f9ecff4b..c6ed6c7538 100644 --- a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt @@ -56,5 +56,8 @@ add_subdirectory(editor) ## Streaming ## add_subdirectory(streaming) -## Streaming ## +## Smoke ## add_subdirectory(smoke) + +## AWS ## +add_subdirectory(AWS) diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_test_helper.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_test_helper.py index 74f34659c0..d375e69ac2 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_test_helper.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_test_helper.py @@ -98,11 +98,6 @@ class EditorTestHelper: self.test_success = False success = False - # Turn off any display info like FPS, as that will mess up our image comparisons - # Turn off antialiasing as well - general.run_console("r_displayInfo=0") - general.run_console("r_antialiasingmode=0") - general.idle_wait(1.0) return success # Test Teardown diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/pyside_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/pyside_utils.py index 10f060bba6..eed6a18614 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/pyside_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/pyside_utils.py @@ -18,7 +18,6 @@ from PySide2 import QtCore, QtWidgets, QtGui, QtTest from PySide2.QtWidgets import QAction, QWidget from PySide2.QtCore import Qt from PySide2.QtTest import QTest -import azlmbr.legacy.general as general import traceback import threading import types diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomEditorComponents_AddedToEntity.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomEditorComponents_AddedToEntity.py index e701ff8d16..904075c747 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomEditorComponents_AddedToEntity.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_AtomEditorComponents_AddedToEntity.py @@ -77,10 +77,10 @@ def run(): def verify_enter_exit_game_mode(component_name): general.enter_game_mode() - TestHelper.wait_for_condition(lambda: general.is_in_game_mode(), 1.0) + TestHelper.wait_for_condition(lambda: general.is_in_game_mode(), 2.0) general.log(f"{component_name}_test: Entered game mode: {general.is_in_game_mode()}") general.exit_game_mode() - TestHelper.wait_for_condition(lambda: not general.is_in_game_mode(), 1.0) + TestHelper.wait_for_condition(lambda: not general.is_in_game_mode(), 2.0) general.log(f"{component_name}_test: Exit game mode: {not general.is_in_game_mode()}") def verify_hide_unhide_entity(component_name, entity_obj): @@ -97,16 +97,16 @@ def run(): def verify_deletion_undo_redo(component_name, entity_obj): editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntityById", entity_obj.id) - TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 1.0) + TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 2.0) general.log(f"{component_name}_test: Entity deleted: {not hydra.find_entity_by_name(entity_obj.name)}") general.undo() - TestHelper.wait_for_condition(lambda: hydra.find_entity_by_name(entity_obj.name) is not None, 1.0) + TestHelper.wait_for_condition(lambda: hydra.find_entity_by_name(entity_obj.name) is not None, 2.0) general.log(f"{component_name}_test: UNDO entity deletion works: " f"{hydra.find_entity_by_name(entity_obj.name) is not None}") general.redo() - TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 1.0) + TestHelper.wait_for_condition(lambda: not hydra.find_entity_by_name(entity_obj.name), 2.0) general.log(f"{component_name}_test: REDO entity deletion works: " f"{not hydra.find_entity_by_name(entity_obj.name)}") @@ -120,7 +120,7 @@ def run(): f"{not is_component_enabled(entity_obj.components[0])}") for component in components_to_add: entity_obj.add_component(component) - TestHelper.wait_for_condition(lambda: is_component_enabled(entity_obj.components[0]), 1.0) + TestHelper.wait_for_condition(lambda: is_component_enabled(entity_obj.components[0]), 2.0) general.log( f"{component_name}_test: Entity enabled after adding " f"required components: {is_component_enabled(entity_obj.components[0])}" @@ -135,7 +135,9 @@ def run(): # Delete all existing entities initially search_filter = azlmbr.entity.SearchFilter() all_entities = entity.SearchBus(azlmbr.bus.Broadcast, "SearchEntities", search_filter) + general.idle_wait_frames(1) editor.ToolsApplicationRequestBus(bus.Broadcast, "DeleteEntities", all_entities) + general.idle_wait_frames(1) class ComponentTests: """Test launcher for each component.""" @@ -147,9 +149,11 @@ def run(): def run_component_tests(self): # Run common and additional tests entity_obj = create_entity_undo_redo_component_addition(self.component_name) + general.idle_wait(0.5) # Enter/Exit game mode test verify_enter_exit_game_mode(self.component_name) + general.idle_wait(0.5) # Any additional tests are executed here for test in self.additional_tests: @@ -157,13 +161,16 @@ def run(): # Hide/Unhide entity test verify_hide_unhide_entity(self.component_name, entity_obj) + general.idle_wait(0.5) # Deletion/Undo/Redo test verify_deletion_undo_redo(self.component_name, entity_obj) + general.idle_wait(0.5) # DepthOfField Component camera_entity = hydra.Entity("camera_entity") camera_entity.create_entity(math.Vector3(512.0, 512.0, 34.0), ["Camera"]) + general.idle_wait(0.5) depth_of_field = "DepthOfField" ComponentTests( depth_of_field, diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py index 0b75760e05..b64a592c1d 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite.py @@ -163,7 +163,8 @@ class TestAtomEditorComponentsMain(object): ] unexpected_lines = [ - "failed to open", + "Trace::Assert", + "Trace::Error", "Traceback (most recent call last):", ] diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_EditMenuOptions.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_EditMenuOptions.py index 208ef40d41..3eeb3ddf84 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_EditMenuOptions.py +++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_EditMenuOptions.py @@ -64,8 +64,6 @@ class TestEditMenuOptions(EditorTestHelper): ("Modify", "Transform Mode", "Move"), ("Modify", "Transform Mode", "Rotate"), ("Modify", "Transform Mode", "Scale"), - ("Lock Selection",), - ("Unlock All Entities",), ("Editor Settings", "Global Preferences"), ("Editor Settings", "Graphics Settings"), ("Editor Settings", "Editor Settings Manager"), diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py index 05ee802d19..173d658b3c 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py +++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py @@ -50,7 +50,6 @@ class TestViewMenuOptions(EditorTestHelper): ("Center on Selection",), ("Show Quick Access Bar",), ("Viewport", "Wireframe"), - ("Viewport", "Grid Settings"), ("Viewport", "Go to Position"), ("Viewport", "Center on Selection"), ("Viewport", "Go to Location"), diff --git a/AutomatedTesting/Gem/PythonTests/editor/test_Docking.py b/AutomatedTesting/Gem/PythonTests/editor/test_Docking.py index a75b3b36cd..c2d515e250 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/test_Docking.py +++ b/AutomatedTesting/Gem/PythonTests/editor/test_Docking.py @@ -40,7 +40,7 @@ class TestDocking(object): @pytest.mark.test_case_id("C6376081") @pytest.mark.SUITE_periodic - def test_basic_docked_tools(self, request, editor, level, launcher_platform): + def test_Docking_BasicDockedTools(self, request, editor, level, launcher_platform): expected_lines = [ "The tools are all docked together in a tabbed widget", "Entity Outliner works when docked, can select an Entity", diff --git a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py index 251bd7cfed..70a22f9e2a 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py +++ b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py @@ -59,8 +59,6 @@ class TestMenus(object): "Move Action triggered", "Rotate Action triggered", "Scale Action triggered", - "Lock Selection Action triggered", - "Unlock All Entities Action triggered", "Global Preferences Action triggered", "Graphics Settings Action triggered", "Editor Settings Manager Action triggered", @@ -93,7 +91,6 @@ class TestMenus(object): "Center on Selection Action triggered", "Show Quick Access Bar Action triggered", "Wireframe Action triggered", - "Grid Settings Action triggered", "Go to Position Action triggered", "Center on Selection Action triggered", "Go to Location Action triggered", diff --git a/AutomatedTesting/Gem/PythonTests/scripting/Pane_PropertiesChanged_RetainsOnRestart.py b/AutomatedTesting/Gem/PythonTests/scripting/Pane_PropertiesChanged_RetainsOnRestart.py new file mode 100644 index 0000000000..3750fd290b --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/scripting/Pane_PropertiesChanged_RetainsOnRestart.py @@ -0,0 +1,161 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + + +class Tests: + test_panes_visible = "All the test panes are opened" + close_pane_1 = "Test pane 1 is closed" + resize_pane_3 = "Test pane 3 resized successfully" + location_changed = "Location of test pane 2 changed successfully" + visiblity_retained = "Test pane retained its visiblity on Editor restart" + location_retained = "Test pane retained its location on Editor restart" + size_retained = "Test pane retained its size on Editor restart" + + +def Pane_PropertiesChanged_RetainsOnRestart(): + """ + Summary: + The Script Canvas window is opened to verify if Script canvas panes can retain its visibility, size and location + upon Editor restart. + + Expected Behavior: + The ScriptCanvas pane retain it's visiblity, size and location upon Editor restart. + + Test Steps: + 1) Open Script Canvas window (Tools > Script Canvas) + 2) Make sure test panes are open and visible + 3) Close test pane 1 + 4) Change dock location of test pane 2 + 5) Resize test pane 3 + 6) Restart Editor + 7) Verify if test pane 1 retain its visiblity + 8) Verify if location of test pane 2 is retained + 9) Verify if size of test pane 3 is retained + 10) Restore default layout and close SC window + + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. + + :return: None + """ + + import sys + + # Helper imports + from utils import Report + from utils import TestHelper as helper + import pyside_utils + + # Lumberyard Imports + import azlmbr.legacy.general as general + + # Pyside imports + from PySide2 import QtCore, QtWidgets + from PySide2.QtCore import Qt + + # Constants + TEST_CONDITION = sys.argv[1] + TEST_PANE_1 = "NodePalette" # pane used to test visibility + TEST_PANE_2 = "VariableManager" # pane used to test location + TEST_PANE_3 = "NodeInspector" # pane used to test size + SCALE_INT = 10 # Random resize scale integer + DOCKAREA = Qt.TopDockWidgetArea # Preferred top area since no widget is docked on top + + def click_menu_option(window, option_text): + action = pyside_utils.find_child_by_pattern(window, {"text": option_text, "type": QtWidgets.QAction}) + action.trigger() + + def find_pane(window, pane_name): + return window.findChild(QtWidgets.QDockWidget, pane_name) + + # Test starts here + general.idle_enable(True) + + # 1) Open Script Canvas window (Tools > Script Canvas) + general.open_pane("Script Canvas") + helper.wait_for_condition(lambda: general.is_pane_visible("Script Canvas"), 5.0) + + if TEST_CONDITION == "before_restart": + # 2) Make sure test panes are open and visible + editor_window = pyside_utils.get_editor_main_window() + sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas") + click_menu_option(sc, "Restore Default Layout") + test_pane_1 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_1) + test_pane_2 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_2) + test_pane_3 = sc.findChild(QtWidgets.QDockWidget, TEST_PANE_3) + + result = test_pane_1.isVisible() and test_pane_2.isVisible() and test_pane_3.isVisible() + Report.info(f"{Tests.test_panes_visible}: {result}") + + # 3) Close test pane + test_pane_1.close() + Report.info(f"{Tests.close_pane_1}: {not test_pane_1.isVisible()}") + + # 4) Change dock location of test pane 2 + sc_main = sc.findChild(QtWidgets.QMainWindow) + sc_main.addDockWidget(DOCKAREA, find_pane(sc_main, TEST_PANE_2), QtCore.Qt.Vertical) + Report.info(f"{Tests.location_changed}: {sc_main.dockWidgetArea(find_pane(sc_main, TEST_PANE_2)) == DOCKAREA}") + + # 5) Resize test pane 3 + initial_size = test_pane_3.frameSize() + test_pane_3.resize(initial_size.width() + SCALE_INT, initial_size.height() + SCALE_INT) + new_size = test_pane_3.frameSize() + resize_success = ( + abs(initial_size.width() - new_size.width()) == abs(initial_size.height() - new_size.height()) == SCALE_INT + ) + Report.info(f"{Tests.resize_pane_3}: {resize_success}") + + if TEST_CONDITION == "after_restart": + try: + # 6) Restart Editor + # Restart is not possible through script and hence it is done by running the same file as 2 tests with a + # condition as before_test and after_test + + # 7) Verify if test pane 1 retain its visiblity + # This pane closed before restart and expected that pane should not be visible. + editor_window = pyside_utils.get_editor_main_window() + sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas") + Report.info(f"{Tests.visiblity_retained}: {not find_pane(sc, TEST_PANE_1).isVisible()}") + + # 8) Verify if location of test pane 2 is retained + # This pane was set at DOCKAREA lcoation before restart + sc_main = sc.findChild(QtWidgets.QMainWindow) + Report.info( + f"{Tests.location_retained}: {sc_main.dockWidgetArea(find_pane(sc_main, TEST_PANE_2)) == DOCKAREA}" + ) + + # 9) Verify if size of test pane 3 is retained + # Verifying if size retained by checking current size not matching with default size + test_pane_3 = find_pane(sc, TEST_PANE_3) + retained_size = test_pane_3.frameSize() + click_menu_option(sc, "Restore Default Layout") + actual_size = test_pane_3.frameSize() + Report.info(f"{Tests.size_retained}: {retained_size != actual_size}") + + finally: + # 10) Restore default layout and close SC window + general.open_pane("Script Canvas") + helper.wait_for_condition(lambda: general.is_pane_visible("Script Canvas"), 5.0) + sc = editor_window.findChild(QtWidgets.QDockWidget, "Script Canvas") + click_menu_option(sc, "Restore Default Layout") + sc.close() + + +if __name__ == "__main__": + import ImportPathHelper as imports + + imports.init() + + from utils import Report + + Report.start_test(Pane_PropertiesChanged_RetainsOnRestart) diff --git a/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py b/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py index c42c9f1a03..9180c1b44c 100755 --- a/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py +++ b/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py @@ -76,14 +76,8 @@ class TestAutomation(TestAutomationBase): from . import ScriptCanvasComponent_OnEntityActivatedDeactivated_PrintMessage as test_module self._run_test(request, workspace, editor, test_module) -<<<<<<< HEAD def test_NodePalette_HappyPath_ClearSelection(self, request, workspace, editor, launcher_platform, project): from . import NodePalette_HappyPath_ClearSelection as test_module -======= - @pytest.mark.test_case_id("T92562993") - def test_NodePalette_ClearSelection(self, request, workspace, editor, launcher_platform, project): - from . import NodePalette_ClearSelection as test_module ->>>>>>> main self._run_test(request, workspace, editor, test_module) @pytest.mark.parametrize("level", ["tmp_level"]) @@ -119,7 +113,6 @@ class TestAutomation(TestAutomationBase): from . import Debugger_HappyPath_TargetMultipleGraphs as test_module self._run_test(request, workspace, editor, test_module) - @pytest.mark.test_case_id("T92569137") def test_Debugging_TargetMultipleGraphs(self, request, workspace, editor, launcher_platform, project): from . import Debugging_TargetMultipleGraphs as test_module self._run_test(request, workspace, editor, test_module) @@ -262,3 +255,37 @@ class TestScriptCanvasTests(object): auto_test_mode=False, timeout=60, ) + + @pytest.mark.parametrize( + "config", + [ + { + "cfg_args": "before_restart", + "expected_lines": [ + "All the test panes are opened: True", + "Test pane 1 is closed: True", + "Location of test pane 2 changed successfully: True", + "Test pane 3 resized successfully: True", + ], + }, + { + "cfg_args": "after_restart", + "expected_lines": [ + "Test pane retained its visiblity on Editor restart: True", + "Test pane retained its location on Editor restart: True", + "Test pane retained its size on Editor restart: True", + ], + }, + ], + ) + def test_Pane_PropertiesChanged_RetainsOnRestart(self, request, editor, config, project, launcher_platform): + hydra.launch_and_validate_results( + request, + TEST_DIRECTORY, + editor, + "Pane_PropertiesChanged_RetainsOnRestart.py", + config.get('expected_lines'), + cfg_args=[config.get('cfg_args')], + auto_test_mode=False, + timeout=60, + ) diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/ClientAuth.ly b/AutomatedTesting/Levels/AWS/ClientAuth/ClientAuth.ly new file mode 100644 index 0000000000..af8a7f5c8e --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/ClientAuth.ly @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4d4e0155feaa76c80a14128000a0fd9570ab76e79f4847eaef9006324a4d2 +size 9084 diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/ConitoAnonymousAuthorization.scriptcanvas b/AutomatedTesting/Levels/AWS/ClientAuth/ConitoAnonymousAuthorization.scriptcanvas new file mode 100644 index 0000000000..ef03c66b16 --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/ConitoAnonymousAuthorization.scriptcanvas @@ -0,0 +1,2313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/Environment.xml b/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/Environment.xml new file mode 100644 index 0000000000..d4e3d33551 --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/Environment.xml @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/TimeOfDay.xml b/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/TimeOfDay.xml new file mode 100644 index 0000000000..d827d4da29 --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/LevelData/TimeOfDay.xml @@ -0,0 +1 @@ + diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/filelist.xml b/AutomatedTesting/Levels/AWS/ClientAuth/filelist.xml new file mode 100644 index 0000000000..f69a99fe37 --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/filelist.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/level.pak b/AutomatedTesting/Levels/AWS/ClientAuth/level.pak new file mode 100644 index 0000000000..1ae0bb1f7a --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/level.pak @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4900bdf28654e21032e69957f2762fa0a3b93a4b82163267a1f10f19f6d78692 +size 3795 diff --git a/AutomatedTesting/Levels/AWS/ClientAuth/tags.txt b/AutomatedTesting/Levels/AWS/ClientAuth/tags.txt new file mode 100644 index 0000000000..0d6c1880e7 --- /dev/null +++ b/AutomatedTesting/Levels/AWS/ClientAuth/tags.txt @@ -0,0 +1,12 @@ +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 +0,0,0,0,0,0 diff --git a/AutomatedTesting/Registry/awscoreconfiguration.setreg b/AutomatedTesting/Registry/awscoreconfiguration.setreg new file mode 100644 index 0000000000..ca110eb103 --- /dev/null +++ b/AutomatedTesting/Registry/awscoreconfiguration.setreg @@ -0,0 +1,10 @@ +{ + "Amazon": + { + "AWSCore": + { + "ProfileName": "default", + "ResourceMappingConfigFileName": "aws_resource_mappings.json" + } + } +} \ No newline at end of file diff --git a/Code/CryEngine/CrySystem/DebugCallStack.cpp b/Code/CryEngine/CrySystem/DebugCallStack.cpp new file mode 100644 index 0000000000..2a219ce674 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.cpp @@ -0,0 +1,900 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +#include "CrySystem_precompiled.h" +#include "DebugCallStack.h" + +#if defined(WIN32) || defined(WIN64) + +#include +#include +#include "System.h" + +#include +#include + +#define VS_VERSION_INFO 1 +#define IDD_CRITICAL_ERROR 101 +#define IDB_CONFIRM_SAVE 102 +#define IDB_DONT_SAVE 103 +#define IDD_CONFIRM_SAVE_LEVEL 127 +#define IDB_CRASH_FACE 128 +#define IDD_EXCEPTION 245 +#define IDC_CALLSTACK 1001 +#define IDC_EXCEPTION_CODE 1002 +#define IDC_EXCEPTION_ADDRESS 1003 +#define IDC_EXCEPTION_MODULE 1004 +#define IDC_EXCEPTION_DESC 1005 +#define IDB_EXIT 1008 +#define IDB_IGNORE 1010 +__pragma(comment(lib, "version.lib")) + +//! Needs one external of DLL handle. +extern HMODULE gDLLHandle; + +#include + +#define MAX_PATH_LENGTH 1024 +#define MAX_SYMBOL_LENGTH 512 + +static HWND hwndException = 0; +static bool g_bUserDialog = true; // true=on crash show dialog box, false=supress user interaction + +static int PrintException(EXCEPTION_POINTERS* pex); + +static bool IsFloatingPointException(EXCEPTION_POINTERS* pex); + +extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers); +extern LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE mdumpValue); + +//============================================================================= +CONTEXT CaptureCurrentContext() +{ + CONTEXT context; + memset(&context, 0, sizeof(context)); + context.ContextFlags = CONTEXT_FULL; + RtlCaptureContext(&context); + + return context; +} + +LONG __stdcall CryUnhandledExceptionHandler(EXCEPTION_POINTERS* pex) +{ + return DebugCallStack::instance()->handleException(pex); +} + + +BOOL CALLBACK EnumModules( + PCSTR ModuleName, + DWORD64 BaseOfDll, + PVOID UserContext) +{ + DebugCallStack::TModules& modules = *static_cast(UserContext); + modules[(void*)BaseOfDll] = ModuleName; + + return TRUE; +} +//============================================================================= +// Class Statics +//============================================================================= + +// Return single instance of class. +IDebugCallStack* IDebugCallStack::instance() +{ + static DebugCallStack sInstance; + return &sInstance; +} + +//------------------------------------------------------------------------------------------------------------------------ +// Sets up the symbols for functions in the debug file. +//------------------------------------------------------------------------------------------------------------------------ +DebugCallStack::DebugCallStack() + : prevExceptionHandler(0) + , m_pSystem(0) + , m_nSkipNumFunctions(0) + , m_bCrash(false) + , m_szBugMessage(NULL) +{ +} + +DebugCallStack::~DebugCallStack() +{ +} + +void DebugCallStack::RemoveOldFiles() +{ + RemoveFile("error.log"); + RemoveFile("error.bmp"); + RemoveFile("error.dmp"); +} + +void DebugCallStack::RemoveFile(const char* szFileName) +{ + FILE* pFile = nullptr; + azfopen(&pFile, szFileName, "r"); + const bool bFileExists = (pFile != NULL); + + if (bFileExists) + { + fclose(pFile); + + WriteLineToLog("Removing file \"%s\"...", szFileName); + if (remove(szFileName) == 0) + { + WriteLineToLog("File successfully removed."); + } + else + { + WriteLineToLog("Couldn't remove file!"); + } + } +} + +void DebugCallStack::installErrorHandler(ISystem* pSystem) +{ + m_pSystem = pSystem; + prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler); +} + +////////////////////////////////////////////////////////////////////////// +void DebugCallStack::SetUserDialogEnable(const bool bUserDialogEnable) +{ + g_bUserDialog = bUserDialogEnable; +} + + +DWORD g_idDebugThreads[10]; +const char* g_nameDebugThreads[10]; +int g_nDebugThreads = 0; +volatile int g_lockThreadDumpList = 0; + +void MarkThisThreadForDebugging(const char* name) +{ + EBUS_EVENT(AZ::Debug::EventTraceDrillerSetupBus, SetThreadName, AZStd::this_thread::get_id(), name); + + WriteLock lock(g_lockThreadDumpList); + DWORD id = GetCurrentThreadId(); + if (g_nDebugThreads == sizeof(g_idDebugThreads) / sizeof(g_idDebugThreads[0])) + { + return; + } + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] == id) + { + return; + } + } + g_nameDebugThreads[g_nDebugThreads] = name; + g_idDebugThreads[g_nDebugThreads++] = id; + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions); +} + +void UnmarkThisThreadFromDebugging() +{ + WriteLock lock(g_lockThreadDumpList); + DWORD id = GetCurrentThreadId(); + for (int i = g_nDebugThreads - 1; i >= 0; i--) + { + if (g_idDebugThreads[i] == id) + { + memmove(g_idDebugThreads + i, g_idDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_idDebugThreads[0])); + memmove(g_nameDebugThreads + i, g_nameDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_nameDebugThreads[0])); + --g_nDebugThreads; + } + } +} + +extern int prev_sys_float_exceptions; +void UpdateFPExceptionsMaskForThreads() +{ + int mask = -iszero(g_cvars.sys_float_exceptions); + CONTEXT ctx; + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]); + ctx.ContextFlags = CONTEXT_ALL; + SuspendThread(hThread); + GetThreadContext(hThread, &ctx); +#ifndef WIN64 + (ctx.FloatSave.ControlWord |= 7) &= ~5 | mask; + (*(WORD*)(ctx.ExtendedRegisters + 24) |= 0x280) &= ~0x280 | mask; +#else + (ctx.FltSave.ControlWord |= 7) &= ~5 | mask; + (ctx.FltSave.MxCsr |= 0x280) &= ~0x280 | mask; +#endif + SetThreadContext(hThread, &ctx); + ResumeThread(hThread); + } + } +} + +////////////////////////////////////////////////////////////////////////// +int DebugCallStack::handleException(EXCEPTION_POINTERS* exception_pointer) +{ + if (gEnv == NULL) + { + return EXCEPTION_EXECUTE_HANDLER; + } + + ResetFPU(exception_pointer); + + prev_sys_float_exceptions = 0; + const int cached_sys_float_exceptions = g_cvars.sys_float_exceptions; + + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(0); + + if (g_cvars.sys_WER) + { + gEnv->pLog->FlushAndClose(); + return CryEngineExceptionFilterWER(exception_pointer); + } + + if (g_cvars.sys_no_crash_dialog) + { + DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX); + SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX); + } + + m_bCrash = true; + + if (g_cvars.sys_no_crash_dialog) + { + DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX); + SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX); + } + + static bool firstTime = true; + + if (g_cvars.sys_dump_aux_threads) + { + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + SuspendThread(OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i])); + } + } + } + + // uninstall our exception handler. + SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)prevExceptionHandler); + + if (!firstTime) + { + WriteLineToLog("Critical Exception! Called Multiple Times!"); + gEnv->pLog->FlushAndClose(); + // Exception called more then once. + return EXCEPTION_EXECUTE_HANDLER; + } + + // Print exception info: + { + char excCode[80]; + char excAddr[80]; + WriteLineToLog(""); + sprintf_s(excAddr, "0x%04X:0x%p", exception_pointer->ContextRecord->SegCs, exception_pointer->ExceptionRecord->ExceptionAddress); + sprintf_s(excCode, "0x%08X", exception_pointer->ExceptionRecord->ExceptionCode); + WriteLineToLog("Exception: %s, at Address: %s", excCode, excAddr); + } + + firstTime = false; + + const int ret = SubmitBug(exception_pointer); + + if (ret != IDB_IGNORE) + { + CryEngineExceptionFilterWER(exception_pointer); + } + + gEnv->pLog->FlushAndClose(); + + if (exception_pointer->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) + { + // This is non continuable exception. abort application now. + exit(exception_pointer->ExceptionRecord->ExceptionCode); + } + + //typedef long (__stdcall *ExceptionFunc)(EXCEPTION_POINTERS*); + //ExceptionFunc prevFunc = (ExceptionFunc)prevExceptionHandler; + //return prevFunc( (EXCEPTION_POINTERS*)exception_pointer ); + if (ret == IDB_EXIT) + { + // Immediate exit. + // on windows, exit() and _exit() do all sorts of things, unfortuantely + // TerminateProcess is the only way to die. + TerminateProcess(GetCurrentProcess(), exception_pointer->ExceptionRecord->ExceptionCode); // we crashed, so don't return a zero exit code! + // on linux based systems, _exit will not call ATEXIT and other things, which makes it more suitable for termination in an emergency such + // as an unhandled exception. + // however, this function is a windows exception handler. + } + else if (ret == IDB_IGNORE) + { +#ifndef WIN64 + exception_pointer->ContextRecord->FloatSave.StatusWord &= ~31; + exception_pointer->ContextRecord->FloatSave.ControlWord |= 7; + (*(WORD*)(exception_pointer->ContextRecord->ExtendedRegisters + 24) &= 31) |= 0x1F80; +#else + exception_pointer->ContextRecord->FltSave.StatusWord &= ~31; + exception_pointer->ContextRecord->FltSave.ControlWord |= 7; + (exception_pointer->ContextRecord->FltSave.MxCsr &= 31) |= 0x1F80; +#endif + firstTime = true; + prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler); + g_cvars.sys_float_exceptions = cached_sys_float_exceptions; + ((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions); + return EXCEPTION_CONTINUE_EXECUTION; + } + + // Continue; + return EXCEPTION_EXECUTE_HANDLER; +} + +void DebugCallStack::ReportBug(const char* szErrorMessage) +{ + WriteLineToLog("Reporting bug: %s", szErrorMessage); + + m_szBugMessage = szErrorMessage; + m_context = CaptureCurrentContext(); + SubmitBug(NULL); + m_szBugMessage = NULL; +} + +void DebugCallStack::dumpCallStack(std::vector& funcs) +{ + WriteLineToLog("============================================================================="); + int len = (int)funcs.size(); + for (int i = 0; i < len; i++) + { + const char* str = funcs[i].c_str(); + WriteLineToLog("%2d) %s", len - i, str); + } + WriteLineToLog("============================================================================="); +} + + +////////////////////////////////////////////////////////////////////////// +void DebugCallStack::LogExceptionInfo(EXCEPTION_POINTERS* pex) +{ + string path(""); + if ((gEnv) && (gEnv->pFileIO)) + { + const char* logAlias = gEnv->pFileIO->GetAlias("@log@"); + if (!logAlias) + { + logAlias = gEnv->pFileIO->GetAlias("@root@"); + } + if (logAlias) + { + path = logAlias; + path += "/"; + } + } + + string fileName = path; + fileName += "error.log"; + + struct stat fileInfo; + string timeStamp; + string backupPath; + if (gEnv->IsDedicated()) + { + backupPath = PathUtil::ToUnixPath(PathUtil::AddSlash(path + "DumpBackups")); + gEnv->pFileIO->CreatePath(backupPath.c_str()); + + if (stat(fileName.c_str(), &fileInfo) == 0) + { + // Backup log + tm creationTime; + localtime_s(&creationTime, &fileInfo.st_mtime); + char tempBuffer[32]; + strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime); + timeStamp = tempBuffer; + + string backupFileName = backupPath + timeStamp + " error.log"; + CopyFile(fileName.c_str(), backupFileName.c_str(), true); + } + } + + FILE* f = nullptr; + azfopen(&f, fileName.c_str(), "wt"); + + static char errorString[s_iCallStackSize]; + errorString[0] = 0; + + // Time and Version. + char versionbuf[1024]; + azstrcpy(versionbuf, AZ_ARRAY_SIZE(versionbuf), ""); + PutVersion(versionbuf, AZ_ARRAY_SIZE(versionbuf)); + cry_strcat(errorString, versionbuf); + cry_strcat(errorString, "\n"); + + char excCode[MAX_WARNING_LENGTH]; + char excAddr[80]; + char desc[1024]; + char excDesc[MAX_WARNING_LENGTH]; + + // make sure the mouse cursor is visible + ShowCursor(TRUE); + + const char* excName; + if (m_bIsFatalError || !pex) + { + const char* const szMessage = m_bIsFatalError ? s_szFatalErrorCode : m_szBugMessage; + excName = szMessage; + cry_strcpy(excCode, szMessage); + cry_strcpy(excAddr, ""); + cry_strcpy(desc, ""); + cry_strcpy(m_excModule, ""); + cry_strcpy(excDesc, szMessage); + } + else + { + sprintf_s(excAddr, "0x%04X:0x%p", pex->ContextRecord->SegCs, pex->ExceptionRecord->ExceptionAddress); + sprintf_s(excCode, "0x%08X", pex->ExceptionRecord->ExceptionCode); + excName = TranslateExceptionCode(pex->ExceptionRecord->ExceptionCode); + cry_strcpy(desc, ""); + sprintf_s(excDesc, "%s\r\n%s", excName, desc); + + + if (pex->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) + { + if (pex->ExceptionRecord->NumberParameters > 1) + { + ULONG_PTR iswrite = pex->ExceptionRecord->ExceptionInformation[0]; + DWORD64 accessAddr = pex->ExceptionRecord->ExceptionInformation[1]; + if (iswrite) + { + sprintf_s(desc, "Attempt to write data to address 0x%08llu\r\nThe memory could not be \"written\"", accessAddr); + } + else + { + sprintf_s(desc, "Attempt to read from address 0x%08llu\r\nThe memory could not be \"read\"", accessAddr); + } + } + } + } + + + WriteLineToLog("Exception Code: %s", excCode); + WriteLineToLog("Exception Addr: %s", excAddr); + WriteLineToLog("Exception Module: %s", m_excModule); + WriteLineToLog("Exception Name : %s", excName); + WriteLineToLog("Exception Description: %s", desc); + + + cry_strcpy(m_excDesc, excDesc); + cry_strcpy(m_excAddr, excAddr); + cry_strcpy(m_excCode, excCode); + + + char errs[32768]; + sprintf_s(errs, "Exception Code: %s\nException Addr: %s\nException Module: %s\nException Description: %s, %s\n", + excCode, excAddr, m_excModule, excName, desc); + + + cry_strcat(errs, "\nCall Stack Trace:\n"); + + std::vector funcs; + { + AZ::Debug::StackFrame frames[25]; + AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)]; + unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 3); + if (numFrames) + { + AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines); + for (unsigned int i = 0; i < numFrames; i++) + { + funcs.push_back(lines[i]); + } + } + dumpCallStack(funcs); + // Fill call stack. + char str[s_iCallStackSize]; + cry_strcpy(str, ""); + for (unsigned int i = 0; i < funcs.size(); i++) + { + char temp[s_iCallStackSize]; + sprintf_s(temp, "%2zd) %s", funcs.size() - i, (const char*)funcs[i].c_str()); + cry_strcat(str, temp); + cry_strcat(str, "\r\n"); + cry_strcat(errs, temp); + cry_strcat(errs, "\n"); + } + cry_strcpy(m_excCallstack, str); + } + + cry_strcat(errorString, errs); + + if (f) + { + fwrite(errorString, strlen(errorString), 1, f); + { + if (g_cvars.sys_dump_aux_threads) + { + for (int i = 0; i < g_nDebugThreads; i++) + { + if (g_idDebugThreads[i] != GetCurrentThreadId()) + { + fprintf(f, "\n\nSuspended thread (%s):\n", g_nameDebugThreads[i]); + HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]); + + // mirrors the AZ::Debug::Trace::PrintCallstack() functionality, but prints to a file + { + AZ::Debug::StackFrame frames[10]; + + // Without StackFrame explicit alignment frames array is aligned to 4 bytes + // which causes the stack tracing to fail. + AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)]; + + unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 0, hThread); + if (numFrames) + { + AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines); + for (unsigned int i2 = 0; i2 < numFrames; ++i2) + { + fprintf(f, "%2d) %s\n", numFrames - i2, lines[i2]); + } + } + } + + ResumeThread(hThread); + } + } + } + } + fflush(f); + fclose(f); + } + + if (pex) + { + MINIDUMP_TYPE mdumpValue; + bool bDump = true; + switch (g_cvars.sys_dump_type) + { + case 0: + bDump = false; + break; + case 1: + mdumpValue = MiniDumpNormal; + break; + case 2: + mdumpValue = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithDataSegs); + break; + case 3: + mdumpValue = MiniDumpWithFullMemory; + break; + default: + mdumpValue = (MINIDUMP_TYPE)g_cvars.sys_dump_type; + break; + } + if (bDump) + { + fileName = path + "error.dmp"; + + if (gEnv->IsDedicated() && stat(fileName.c_str(), &fileInfo) == 0) + { + // Backup dump (use timestamp from error.log if available) + if (timeStamp.empty()) + { + tm creationTime; + localtime_s(&creationTime, &fileInfo.st_mtime); + char tempBuffer[32]; + strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime); + timeStamp = tempBuffer; + } + + string backupFileName = backupPath + timeStamp + " error.dmp"; + CopyFile(fileName.c_str(), backupFileName.c_str(), true); + } + + CryEngineExceptionFilterMiniDump(pex, fileName.c_str(), mdumpValue); + } + } + + //if no crash dialog don't even submit the bug + if (m_postBackupProcess && g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog) + { + m_postBackupProcess(); + } + else + { + // lawsonn: Disabling the JIRA-based crash reporter for now + // we'll need to deal with it our own way, pending QA. + // if you're customizing the engine this is also your opportunity to deal with it. + if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog) + { + // ------------ place custom crash handler here --------------------- + // it should launch an executable! + /// by this time, error.bmp will be in the engine root folder + // error.log and error.dmp will also be present in the engine root folder + // if your error dumper wants those, it should zip them up and send them or offer to do so. + // ------------------------------------------------------------------ + } + } + const bool bQuitting = !gEnv || !gEnv->pSystem || gEnv->pSystem->IsQuitting(); + + //[AlexMcC|16.04.10] When the engine is shutting down, MessageBox doesn't display a box + // and immediately returns IDYES. Avoid this by just not trying to save if we're quitting. + // Don't ask to save if this isn't a real crash (a real crash has exception pointers) + if (g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog && gEnv->IsEditor() && !bQuitting && pex) + { + BackupCurrentLevel(); + + const INT_PTR res = DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CONFIRM_SAVE_LEVEL), NULL, DebugCallStack::ConfirmSaveDialogProc, NULL); + if (res == IDB_CONFIRM_SAVE) + { + if (SaveCurrentLevel()) + { + MessageBox(NULL, "Level has been successfully saved!\r\nPress Ok to terminate Editor.", "Save", MB_OK); + } + else + { + MessageBox(NULL, "Error saving level.\r\nPress Ok to terminate Editor.", "Save", MB_OK | MB_ICONWARNING); + } + } + } + + if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog) + { + // terminate immediately - since we're in a crash, there is no point unwinding stack, we've already done access violation or worse. + // calling exit will only cause further death down the line... + TerminateProcess(GetCurrentProcess(), pex->ExceptionRecord->ExceptionCode); + } +} + + +INT_PTR CALLBACK DebugCallStack::ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam) +{ + static EXCEPTION_POINTERS* pex; + + static char errorString[32768] = ""; + + switch (message) + { + case WM_INITDIALOG: + { + pex = (EXCEPTION_POINTERS*)lParam; + HWND h; + + if (pex->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE) + { + // Disable continue button for non continuable exceptions. + //h = GetDlgItem( hwndDlg,IDB_CONTINUE ); + //if (h) EnableWindow( h,FALSE ); + } + + DebugCallStack* pDCS = static_cast(DebugCallStack::instance()); + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_DESC); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excDesc); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_CODE); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excCode); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_MODULE); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excModule); + } + + h = GetDlgItem(hwndDlg, IDC_EXCEPTION_ADDRESS); + if (h) + { + SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excAddr); + } + + // Fill call stack. + HWND callStack = GetDlgItem(hwndDlg, IDC_CALLSTACK); + if (callStack) + { + SendMessage(callStack, WM_SETTEXT, FALSE, (LPARAM)pDCS->m_excCallstack); + } + + if (hwndException) + { + DestroyWindow(hwndException); + hwndException = 0; + } + + if (IsFloatingPointException(pex)) + { + EnableWindow(GetDlgItem(hwndDlg, IDB_IGNORE), TRUE); + } + } + break; + + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case IDB_EXIT: + case IDB_IGNORE: + // Fall through. + + EndDialog(hwndDlg, wParam); + return TRUE; + } + } + return FALSE; +} + +INT_PTR CALLBACK DebugCallStack::ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, [[maybe_unused]] LPARAM lParam) +{ + switch (message) + { + case WM_INITDIALOG: + { + // The user might be holding down the spacebar while the engine crashes. + // If we don't remove keyboard focus from this dialog, the keypress will + // press the default button before the dialog actually appears, even if + // the user has already released the key, which is bad. + SetFocus(NULL); + } break; + case WM_COMMAND: + { + switch (LOWORD(wParam)) + { + case IDB_CONFIRM_SAVE: // Fall through + case IDB_DONT_SAVE: + { + EndDialog(hwndDlg, wParam); + return TRUE; + } + } + } break; + } + + return FALSE; +} + +bool DebugCallStack::BackupCurrentLevel() +{ + CSystem* pSystem = static_cast(m_pSystem); + if (pSystem && pSystem->GetUserCallback()) + { + return pSystem->GetUserCallback()->OnBackupDocument(); + } + + return false; +} + +bool DebugCallStack::SaveCurrentLevel() +{ + CSystem* pSystem = static_cast(m_pSystem); + if (pSystem && pSystem->GetUserCallback()) + { + return pSystem->GetUserCallback()->OnSaveDocument(); + } + + return false; +} + +int DebugCallStack::SubmitBug(EXCEPTION_POINTERS* exception_pointer) +{ + int ret = IDB_EXIT; + + assert(!hwndException); + + RemoveOldFiles(); + + AZ::Debug::Trace::PrintCallstack("", 2); + + LogExceptionInfo(exception_pointer); + + if (IsFloatingPointException(exception_pointer)) + { + //! Print exception dialog. + ret = PrintException(exception_pointer); + } + + return ret; +} + +void DebugCallStack::ResetFPU(EXCEPTION_POINTERS* pex) +{ + if (IsFloatingPointException(pex)) + { + // How to reset FPU: http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_10310953.html + _clearfp(); +#ifndef WIN64 + pex->ContextRecord->FloatSave.ControlWord |= 0x2F; + pex->ContextRecord->FloatSave.StatusWord &= ~0x8080; +#endif + } +} + +string DebugCallStack::GetModuleNameForAddr(void* addr) +{ + if (m_modules.empty()) + { + return "[unknown]"; + } + + if (addr < m_modules.begin()->first) + { + return "[unknown]"; + } + + TModules::const_iterator it = m_modules.begin(); + TModules::const_iterator end = m_modules.end(); + for (; ++it != end; ) + { + if (addr < it->first) + { + return (--it)->second; + } + } + + //if address is higher than the last module, we simply assume it is in the last module. + return m_modules.rbegin()->second; +} + +void DebugCallStack::GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line) +{ + AZ::Debug::SymbolStorage::StackLine func, file, module; + AZ::Debug::SymbolStorage::FindFunctionFromIP(addr, &func, &file, &module, line, baseAddr); + procName = func; + filename = file; +} + +string DebugCallStack::GetCurrentFilename() +{ + char fullpath[MAX_PATH_LENGTH + 1]; + GetModuleFileName(NULL, fullpath, MAX_PATH_LENGTH); + return fullpath; +} + +static bool IsFloatingPointException(EXCEPTION_POINTERS* pex) +{ + if (!pex) + { + return false; + } + + DWORD exceptionCode = pex->ExceptionRecord->ExceptionCode; + switch (exceptionCode) + { + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_UNDERFLOW: + case STATUS_FLOAT_MULTIPLE_FAULTS: + case STATUS_FLOAT_MULTIPLE_TRAPS: + return true; + + default: + return false; + } +} + +int DebugCallStack::PrintException(EXCEPTION_POINTERS* exception_pointer) +{ + return (int)DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CRITICAL_ERROR), NULL, DebugCallStack::ExceptionDialogProc, (LPARAM)exception_pointer); +} + +#else +void MarkThisThreadForDebugging(const char*) {} +void UnmarkThisThreadFromDebugging() {} +void UpdateFPExceptionsMaskForThreads() {} +#endif //WIN32 diff --git a/Code/CryEngine/CrySystem/DebugCallStack.h b/Code/CryEngine/CrySystem/DebugCallStack.h new file mode 100644 index 0000000000..c37e6ba0d4 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.h @@ -0,0 +1,95 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +#ifndef CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H +#define CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H +#pragma once + + +#include "IDebugCallStack.h" + +#if defined (WIN32) || defined (WIN64) + +//! Limits the maximal number of functions in call stack. +const int MAX_DEBUG_STACK_ENTRIES_FILE_DUMP = 12; + +struct ISystem; + +//!============================================================================ +//! +//! DebugCallStack class, capture call stack information from symbol files. +//! +//!============================================================================ +class DebugCallStack + : public IDebugCallStack +{ +public: + DebugCallStack(); + virtual ~DebugCallStack(); + + ISystem* GetSystem() { return m_pSystem; }; + + virtual string GetModuleNameForAddr(void* addr); + virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line); + virtual string GetCurrentFilename(); + + void installErrorHandler(ISystem* pSystem); + virtual int handleException(EXCEPTION_POINTERS* exception_pointer); + + virtual void ReportBug(const char*); + + void dumpCallStack(std::vector& functions); + + void SetUserDialogEnable(const bool bUserDialogEnable); + + typedef std::map TModules; +protected: + static void RemoveOldFiles(); + static void RemoveFile(const char* szFileName); + + static int PrintException(EXCEPTION_POINTERS* exception_pointer); + static INT_PTR CALLBACK ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam); + static INT_PTR CALLBACK ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam); + + void LogExceptionInfo(EXCEPTION_POINTERS* exception_pointer); + bool BackupCurrentLevel(); + bool SaveCurrentLevel(); + int SubmitBug(EXCEPTION_POINTERS* exception_pointer); + void ResetFPU(EXCEPTION_POINTERS* pex); + + static const int s_iCallStackSize = 32768; + + char m_excLine[256]; + char m_excModule[128]; + + char m_excDesc[MAX_WARNING_LENGTH]; + char m_excCode[MAX_WARNING_LENGTH]; + char m_excAddr[80]; + char m_excCallstack[s_iCallStackSize]; + + void* prevExceptionHandler; + + bool m_bCrash; + const char* m_szBugMessage; + + ISystem* m_pSystem; + + int m_nSkipNumFunctions; + CONTEXT m_context; + + TModules m_modules; +}; + +#endif //WIN32 + +#endif // CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H diff --git a/Code/CryEngine/CrySystem/DllMain.cpp b/Code/CryEngine/CrySystem/DllMain.cpp index 7fd620835b..53593821d9 100644 --- a/Code/CryEngine/CrySystem/DllMain.cpp +++ b/Code/CryEngine/CrySystem/DllMain.cpp @@ -14,6 +14,7 @@ #include "CrySystem_precompiled.h" #include "System.h" #include +#include "DebugCallStack.h" #if defined(AZ_RESTRICTED_PLATFORM) #undef AZ_RESTRICTED_SECTION @@ -87,6 +88,16 @@ CRYSYSTEM_API ISystem* CreateSystemInterface(const SSystemInitParams& startupPar startupParams.pUserCallback->OnSystemConnect(pSystem); } +#if defined(WIN32) + // Environment Variable to signal we don't want to override our exception handler - our crash report system will set this + auto envVar = AZ::Environment::FindVariable("ExceptionHandlerIsSet"); + const bool handlerIsSet = (envVar && *envVar); + if (!handlerIsSet) + { + ((DebugCallStack*)IDebugCallStack::instance())->installErrorHandler(pSystem); + } +#endif + bool retVal = false; { AZ::Debug::StartupLogSinkReporter initLogSink; diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.cpp b/Code/CryEngine/CrySystem/IDebugCallStack.cpp new file mode 100644 index 0000000000..c14dd2b0da --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.cpp @@ -0,0 +1,275 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : A multiplatform base class for handling errors and collecting call stacks + + +#include "CrySystem_precompiled.h" +#include "IDebugCallStack.h" +#include "System.h" +#include +#include +#include +#include +//#if !defined(LINUX) + +#include + +const char* const IDebugCallStack::s_szFatalErrorCode = "FATAL_ERROR"; + +IDebugCallStack::IDebugCallStack() + : m_bIsFatalError(false) + , m_postBackupProcess(0) + , m_memAllocFileHandle(AZ::IO::InvalidHandle) +{ +} + +IDebugCallStack::~IDebugCallStack() +{ + StopMemLog(); +} + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_SINGLETON +IDebugCallStack* IDebugCallStack::instance() +{ + static IDebugCallStack sInstance; + return &sInstance; +} +#endif + +void IDebugCallStack::FileCreationCallback(void (* postBackupProcess)()) +{ + m_postBackupProcess = postBackupProcess; +} +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::LogCallstack() +{ + AZ::Debug::Trace::PrintCallstack("", 2); +} + +const char* IDebugCallStack::TranslateExceptionCode(DWORD dwExcept) +{ + switch (dwExcept) + { +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_TRANSLATE + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + break; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + break; + case EXCEPTION_BREAKPOINT: + return "EXCEPTION_BREAKPOINT"; + break; + case EXCEPTION_SINGLE_STEP: + return "EXCEPTION_SINGLE_STEP"; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED"; + break; + case EXCEPTION_FLT_DENORMAL_OPERAND: + return "EXCEPTION_FLT_DENORMAL_OPERAND"; + break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_FLT_INEXACT_RESULT: + return "EXCEPTION_FLT_INEXACT_RESULT"; + break; + case EXCEPTION_FLT_INVALID_OPERATION: + return "EXCEPTION_FLT_INVALID_OPERATION"; + break; + case EXCEPTION_FLT_OVERFLOW: + return "EXCEPTION_FLT_OVERFLOW"; + break; + case EXCEPTION_FLT_STACK_CHECK: + return "EXCEPTION_FLT_STACK_CHECK"; + break; + case EXCEPTION_FLT_UNDERFLOW: + return "EXCEPTION_FLT_UNDERFLOW"; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + break; + case EXCEPTION_INT_OVERFLOW: + return "EXCEPTION_INT_OVERFLOW"; + break; + case EXCEPTION_PRIV_INSTRUCTION: + return "EXCEPTION_PRIV_INSTRUCTION"; + break; + case EXCEPTION_IN_PAGE_ERROR: + return "EXCEPTION_IN_PAGE_ERROR"; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + break; + case EXCEPTION_NONCONTINUABLE_EXCEPTION: + return "EXCEPTION_NONCONTINUABLE_EXCEPTION"; + break; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + break; + case EXCEPTION_INVALID_DISPOSITION: + return "EXCEPTION_INVALID_DISPOSITION"; + break; + case EXCEPTION_GUARD_PAGE: + return "EXCEPTION_GUARD_PAGE"; + break; + case EXCEPTION_INVALID_HANDLE: + return "EXCEPTION_INVALID_HANDLE"; + break; + //case EXCEPTION_POSSIBLE_DEADLOCK: return "EXCEPTION_POSSIBLE_DEADLOCK"; break ; + + case STATUS_FLOAT_MULTIPLE_FAULTS: + return "STATUS_FLOAT_MULTIPLE_FAULTS"; + break; + case STATUS_FLOAT_MULTIPLE_TRAPS: + return "STATUS_FLOAT_MULTIPLE_TRAPS"; + break; + + +#endif + default: + return "Unknown"; + break; + } +} + +void IDebugCallStack::PutVersion(char* str, size_t length) +{ +AZ_PUSH_DISABLE_WARNING(4996, "-Wunknown-warning-option") + + if (!gEnv || !gEnv->pSystem) + { + return; + } + + char sFileVersion[128]; + gEnv->pSystem->GetFileVersion().ToString(sFileVersion, sizeof(sFileVersion)); + + char sProductVersion[128]; + gEnv->pSystem->GetProductVersion().ToString(sProductVersion, sizeof(sFileVersion)); + + + //! Get time. + time_t ltime; + time(<ime); + tm* today = localtime(<ime); + + char s[1024]; + //! Use strftime to build a customized time string. + strftime(s, 128, "Logged at %#c\n", today); + azstrcat(str, length, s); + sprintf_s(s, "FileVersion: %s\n", sFileVersion); + azstrcat(str, length, s); + sprintf_s(s, "ProductVersion: %s\n", sProductVersion); + azstrcat(str, length, s); + + if (gEnv->pLog) + { + const char* logfile = gEnv->pLog->GetFileName(); + if (logfile) + { + sprintf (s, "LogFile: %s\n", logfile); + azstrcat(str, length, s); + } + } + + AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath(); + azstrcat(str, length, "ProjectDir: "); + azstrcat(str, length, projectPath.c_str()); + azstrcat(str, length, "\n"); + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_APPEND_MODULENAME + GetModuleFileNameA(NULL, s, sizeof(s)); + + // Log EXE filename only if possible (not full EXE path which could contain sensitive info) + AZStd::string exeName; + if (AZ::StringFunc::Path::GetFullFileName(s, exeName)) + { + azstrcat(str, length, "Executable: "); + azstrcat(str, length, exeName.c_str()); + +# ifdef AZ_DEBUG_BUILD + azstrcat(str, length, " (debug: yes"); +# else + azstrcat(str, length, " (debug: no"); +# endif + } +#endif +AZ_POP_DISABLE_WARNING +} + + +//Crash the application, in this way the debug callstack routine will be called and it will create all the necessary files (error.log, dump, and eventually screenshot) +void IDebugCallStack::FatalError(const char* description) +{ + m_bIsFatalError = true; + WriteLineToLog(description); + +#ifndef _RELEASE + bool bShowDebugScreen = g_cvars.sys_no_crash_dialog == 0; + // showing the debug screen is not safe when not called from mainthread + // it normally leads to a infinity recursion followed by a stack overflow, preventing + // useful call stacks, thus they are disabled + bShowDebugScreen = bShowDebugScreen && gEnv->mMainThreadId == CryGetCurrentThreadId(); + if (bShowDebugScreen) + { + EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "Open 3D Engine Fatal Error", description, false); + } +#endif + +#if defined(WIN32) || !defined(_RELEASE) + int* p = 0x0; + PREFAST_SUPPRESS_WARNING(6011) * p = 1; // we're intentionally crashing here +#endif +} + +void IDebugCallStack::WriteLineToLog(const char* format, ...) +{ + va_list ArgList; + char szBuffer[MAX_WARNING_LENGTH]; + va_start(ArgList, format); + vsnprintf_s(szBuffer, sizeof(szBuffer), sizeof(szBuffer) - 1, format, ArgList); + cry_strcat(szBuffer, "\n"); + szBuffer[sizeof(szBuffer) - 1] = '\0'; + va_end(ArgList); + + AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle; + AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\error.log", AZ::IO::GetOpenModeFromStringMode("a+t"), fileHandle); + if (fileHandle != AZ::IO::InvalidHandle) + { + AZ::IO::FileIOBase::GetDirectInstance()->Write(fileHandle, szBuffer, strlen(szBuffer)); + AZ::IO::FileIOBase::GetDirectInstance()->Flush(fileHandle); + AZ::IO::FileIOBase::GetDirectInstance()->Close(fileHandle); + } +} + +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::StartMemLog() +{ + AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\memallocfile.log", AZ::IO::OpenMode::ModeWrite, m_memAllocFileHandle); + + assert(m_memAllocFileHandle != AZ::IO::InvalidHandle); +} + +////////////////////////////////////////////////////////////////////////// +void IDebugCallStack::StopMemLog() +{ + if (m_memAllocFileHandle != AZ::IO::InvalidHandle) + { + AZ::IO::FileIOBase::GetDirectInstance()->Close(m_memAllocFileHandle); + m_memAllocFileHandle = AZ::IO::InvalidHandle; + } +} +//#endif //!defined(LINUX) diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.h b/Code/CryEngine/CrySystem/IDebugCallStack.h new file mode 100644 index 0000000000..f181b73913 --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.h @@ -0,0 +1,90 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : A multiplatform base class for handling errors and collecting call stacks + +#ifndef CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H +#define CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H +#pragma once + +#include "System.h" + +#if AZ_LEGACY_CRYSYSTEM_TRAIT_FORWARD_EXCEPTION_POINTERS +struct EXCEPTION_POINTERS; +#endif +//! Limits the maximal number of functions in call stack. +enum +{ + MAX_DEBUG_STACK_ENTRIES = 80 +}; + +class IDebugCallStack +{ +public: + // Returns single instance of DebugStack + static IDebugCallStack* instance(); + + virtual int handleException([[maybe_unused]] EXCEPTION_POINTERS* exception_pointer){return 0; } + + // returns the module name of a given address + virtual string GetModuleNameForAddr([[maybe_unused]] void* addr) { return "[unknown]"; } + + // returns the function name of a given address together with source file and line number (if available) of a given address + virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line) + { + filename = "[unknown]"; + line = 0; + baseAddr = addr; +#if defined(PLATFORM_64BIT) + procName.Format("[%016llX]", addr); +#else + procName.Format("[%08X]", addr); +#endif + } + + // returns current filename + virtual string GetCurrentFilename() { return "[unknown]"; } + + //! Dumps Current Call Stack to log. + virtual void LogCallstack(); + //triggers a fatal error, so the DebugCallstack can create the error.log and terminate the application + void FatalError(const char*); + + //Reports a bug and continues execution + virtual void ReportBug(const char*) {} + + virtual void FileCreationCallback(void (* postBackupProcess)()); + + static void WriteLineToLog(const char* format, ...); + + virtual void StartMemLog(); + virtual void StopMemLog(); + +protected: + IDebugCallStack(); + virtual ~IDebugCallStack(); + + static const char* TranslateExceptionCode(DWORD dwExcept); + static void PutVersion(char* str, size_t length); + + bool m_bIsFatalError; + static const char* const s_szFatalErrorCode; + + void (* m_postBackupProcess)(); + + AZ::IO::HandleType m_memAllocFileHandle; +}; + + + +#endif // CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H diff --git a/Code/CryEngine/CrySystem/System.h b/Code/CryEngine/CrySystem/System.h index 631d84d934..b91b1ba059 100644 --- a/Code/CryEngine/CrySystem/System.h +++ b/Code/CryEngine/CrySystem/System.h @@ -208,6 +208,7 @@ struct SSystemCVars int sys_no_crash_dialog; int sys_no_error_report_window; int sys_dump_aux_threads; + int sys_WER; int sys_dump_type; int sys_ai; int sys_entitysystem; diff --git a/Code/CryEngine/CrySystem/SystemInit.cpp b/Code/CryEngine/CrySystem/SystemInit.cpp index a2c21ea04c..25d6b9c601 100644 --- a/Code/CryEngine/CrySystem/SystemInit.cpp +++ b/Code/CryEngine/CrySystem/SystemInit.cpp @@ -121,6 +121,10 @@ # include #endif +#ifdef WIN32 +extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers); +#endif + #if defined(AZ_RESTRICTED_PLATFORM) #define AZ_RESTRICTED_SECTION SYSTEMINIT_CPP_SECTION_14 #include AZ_RESTRICTED_FILE(SystemInit_cpp) @@ -1484,6 +1488,13 @@ AZ_POP_DISABLE_WARNING InlineInitializationProcessing("CSystem::Init LoadConfigurations"); +#ifdef WIN32 + if (g_cvars.sys_WER) + { + SetUnhandledExceptionFilter(CryEngineExceptionFilterWER); + } +#endif + ////////////////////////////////////////////////////////////////////////// // Localization ////////////////////////////////////////////////////////////////////////// @@ -2020,6 +2031,14 @@ void CSystem::CreateSystemVars() REGISTER_CVAR2("sys_update_profile_time", &g_cvars.sys_update_profile_time, 1.0f, 0, "Time to keep updates timings history for."); REGISTER_CVAR2("sys_no_crash_dialog", &g_cvars.sys_no_crash_dialog, m_bNoCrashDialog, VF_NULL, "Whether to disable the crash dialog window"); REGISTER_CVAR2("sys_no_error_report_window", &g_cvars.sys_no_error_report_window, m_bNoErrorReportWindow, VF_NULL, "Whether to disable the error report list"); +#if defined(_RELEASE) + if (!gEnv->IsDedicated()) + { + REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 1, 0, "Enables Windows Error Reporting"); + } +#else + REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 0, 0, "Enables Windows Error Reporting"); +#endif #ifdef USE_HTTP_WEBSOCKETS REGISTER_CVAR2("sys_simple_http_base_port", &g_cvars.sys_simple_http_base_port, 1880, VF_REQUIRE_APP_RESTART, diff --git a/Code/CryEngine/CrySystem/SystemWin32.cpp b/Code/CryEngine/CrySystem/SystemWin32.cpp index c974365bfc..eb6aa17532 100644 --- a/Code/CryEngine/CrySystem/SystemWin32.cpp +++ b/Code/CryEngine/CrySystem/SystemWin32.cpp @@ -46,6 +46,8 @@ #include #endif +#include "IDebugCallStack.h" + #if defined(APPLE) || defined(LINUX) #include #endif @@ -355,6 +357,7 @@ void CSystem::FatalError(const char* format, ...) } // Dump callstack. + IDebugCallStack::instance()->FatalError(szBuffer); #endif CryDebugBreak(); @@ -396,6 +399,8 @@ void CSystem::ReportBug([[maybe_unused]] const char* format, ...) va_start(ArgList, format); azvsnprintf(szBuffer + strlen(sPrefix), MAX_WARNING_LENGTH - strlen(sPrefix), format, ArgList); va_end(ArgList); + + IDebugCallStack::instance()->ReportBug(szBuffer); #endif } diff --git a/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp new file mode 100644 index 0000000000..feaffd42aa --- /dev/null +++ b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp @@ -0,0 +1,137 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +// Original file Copyright Crytek GMBH or its affiliates, used under license. + +// Description : Support for Windows Error Reporting (WER) + + +#include "CrySystem_precompiled.h" + +#ifdef WIN32 + +#include "System.h" +#include +#include +#include "errorrep.h" +#include "ISystem.h" + +#include + +static WCHAR szPath[MAX_PATH + 1]; +static WCHAR szFR[] = L"\\System32\\FaultRep.dll"; + +WCHAR* GetFullPathToFaultrepDll(void) +{ + UINT rc = GetSystemWindowsDirectoryW(szPath, ARRAYSIZE(szPath)); + if (rc == 0 || rc > ARRAYSIZE(szPath) - ARRAYSIZE(szFR) - 1) + { + return NULL; + } + + wcscat_s(szPath, szFR); + return szPath; +} + + +typedef BOOL (WINAPI * MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType, + CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam + ); + +////////////////////////////////////////////////////////////////////////// +LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE DumpType) +{ + // note: In debug mode, this dll is loaded on startup anyway, so this should not incur an additional load unless it crashes + // very early during startup. + + fflush(nullptr); // according to MSDN on fflush, calling fflush on null flushes all buffers. + HMODULE hndDBGHelpDLL = LoadLibraryA("DBGHELP.DLL"); + + if (!hndDBGHelpDLL) + { + CryLogAlways("Failed to record DMP file: Could not open DBGHELP.DLL"); + return EXCEPTION_CONTINUE_SEARCH; + } + + MINIDUMPWRITEDUMP dumpFnPtr = (MINIDUMPWRITEDUMP)::GetProcAddress(hndDBGHelpDLL, "MiniDumpWriteDump"); + if (!dumpFnPtr) + { + CryLogAlways("Failed to record DMP file: Unable to find MiniDumpWriteDump in DBGHELP.DLL"); + return EXCEPTION_CONTINUE_SEARCH; + } + + HANDLE hFile = ::CreateFile(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + CryLogAlways("Failed to record DMP file: could not open file '%s' for writing - error code: %d", szDumpPath, GetLastError()); + return EXCEPTION_CONTINUE_SEARCH; + } + + _MINIDUMP_EXCEPTION_INFORMATION ExInfo; + ExInfo.ThreadId = ::GetCurrentThreadId(); + ExInfo.ExceptionPointers = pExceptionPointers; + ExInfo.ClientPointers = NULL; + + BOOL bOK = dumpFnPtr(GetCurrentProcess(), GetCurrentProcessId(), hFile, DumpType, &ExInfo, NULL, NULL); + ::CloseHandle(hFile); + + if (bOK) + { + CryLogAlways("Successfully recorded DMP file: '%s'", szDumpPath); + return EXCEPTION_EXECUTE_HANDLER; // SUCCESS! you can execute your handlers now + } + else + { + CryLogAlways("Failed to record DMP file: '%s' - error code: %d", szDumpPath, GetLastError()); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + +////////////////////////////////////////////////////////////////////////// +LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers) +{ + if (g_cvars.sys_WER > 1) + { + char szScratch [_MAX_PATH]; + const char* szDumpPath = gEnv->pCryPak->AdjustFileName("@log@/CE2Dump.dmp", szScratch, AZ_ARRAY_SIZE(szScratch), 0); + + MINIDUMP_TYPE mdumpValue = (MINIDUMP_TYPE)(MiniDumpNormal); + if (g_cvars.sys_WER > 1) + { + mdumpValue = (MINIDUMP_TYPE)(g_cvars.sys_WER - 2); + } + + return CryEngineExceptionFilterMiniDump(pExceptionPointers, szDumpPath, mdumpValue); + } + + LONG lRet = EXCEPTION_CONTINUE_SEARCH; + WCHAR* psz = GetFullPathToFaultrepDll(); + if (psz) + { + HMODULE hFaultRepDll = LoadLibraryW(psz); + if (hFaultRepDll) + { + pfn_REPORTFAULT pfn = (pfn_REPORTFAULT)GetProcAddress(hFaultRepDll, "ReportFault"); + if (pfn) + { + pfn(pExceptionPointers, 0); + lRet = EXCEPTION_EXECUTE_HANDLER; + } + FreeLibrary(hFaultRepDll); + } + } + return lRet; +} + +#endif // WIN32 diff --git a/Code/CryEngine/CrySystem/crysystem_files.cmake b/Code/CryEngine/CrySystem/crysystem_files.cmake index 6a56339b85..84250de95b 100644 --- a/Code/CryEngine/CrySystem/crysystem_files.cmake +++ b/Code/CryEngine/CrySystem/crysystem_files.cmake @@ -15,6 +15,8 @@ set(FILES CmdLineArg.cpp ConsoleBatchFile.cpp ConsoleHelpGen.cpp + DebugCallStack.cpp + IDebugCallStack.cpp Log.cpp System.cpp SystemCFG.cpp @@ -31,6 +33,8 @@ set(FILES CmdLineArg.h ConsoleBatchFile.h ConsoleHelpGen.h + DebugCallStack.h + IDebugCallStack.h Log.h SimpleStringPool.h CrySystem_precompiled.h @@ -72,4 +76,5 @@ set(FILES ViewSystem/ViewSystem.cpp ViewSystem/ViewSystem.h CrySystem_precompiled.cpp + WindowsErrorReporting.cpp ) diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h index c5cdbbf331..11f4531124 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h @@ -307,6 +307,8 @@ namespace AZ Asset(AssetLoadBehavior loadBehavior = AssetLoadBehavior::Default); /// Create an asset from a valid asset data (created asset), might not be loaded or currently loading. Asset(AssetData* assetData, AssetLoadBehavior loadBehavior); + /// Create an asset from a valid asset data (created asset) and set the asset id for both, might not be loaded or currently loading. + Asset(const AZ::Data::AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior); /// Initialize asset pointer with id, type, and hint. No data construction will occur until QueueLoad is called. Asset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint = AZStd::string()); @@ -787,6 +789,18 @@ namespace AZ SetData(assetData); } + //========================================================================= + template + Asset::Asset(const AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior) + : m_assetId(id) + , m_assetType(azrtti_typeid()) + , m_loadBehavior(loadBehavior) + { + AZ_Assert(!assetData->m_assetId.IsValid(), "Asset data already has an ID set."); + assetData->m_assetId = id; + SetData(assetData); + } + //========================================================================= template Asset::Asset(const AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint) diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.cpp index 497d67bef1..cd250b71a9 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.cpp @@ -21,7 +21,7 @@ namespace AzPhysics namespace { const float TimestepMin = 0.001f; //1000fps - const float TimestepMax = 0.05f; //20fps + const float TimestepMax = 0.1f; //10fps } AZ_CLASS_ALLOCATOR_IMPL(SystemConfiguration, AZ::SystemAllocator, 0); diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.h index 3de4dafd8c..0a00d627a7 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/SystemConfiguration.h @@ -34,7 +34,7 @@ namespace AzPhysics static constexpr float DefaultFixedTimestep = 0.0166667f; //! Value represents 1/60th or 60 FPS. - float m_maxTimestep = 1.f / 20.f; //!< Maximum fixed timestep in seconds to run the physics update. + float m_maxTimestep = 0.1f; //!< Maximum fixed timestep in seconds to run the physics update (10FPS). float m_fixedTimestep = DefaultFixedTimestep; //!< Timestep in seconds to run the physics update. See DefaultFixedTimestep. AZ::u64 m_raycastBufferSize = 32; //!< Maximum number of hits that will be returned from a raycast. diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h new file mode 100644 index 0000000000..47388c56c3 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h @@ -0,0 +1,78 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AzFramework +{ + //! SessionConnectionConfig + //! The properties for handling join session request. + struct SessionConnectionConfig + { + // A unique identifier for registered player in session. + AZStd::string m_playerSessionId; + + // The DNS identifier assigned to the instance that is running the session. + AZStd::string m_dnsName; + + // The IP address of the session. + AZStd::string m_ipAddress; + + // The port number for the session. + uint16_t m_port; + }; + + //! SessionConnectionConfig + //! The properties for handling player connect/disconnect + struct PlayerConnectionConfig + { + // A unique identifier for player connection. + uint32_t m_playerConnectionId; + + // A unique identifier for registered player in session. + AZStd::string m_playerSessionId; + }; + + //! ISessionHandlingClientRequests + //! The session handling events to invoke multiplayer component handle the work on client side + class ISessionHandlingClientRequests + { + public: + // Handle the player join session process + // @param sessionConnectionConfig The required properties to handle the player join session process + // @return The result of player join session process + virtual bool HandlePlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0; + + // Handle the player leave session process + virtual void HandlePlayerLeaveSession() = 0; + }; + + //! ISessionHandlingServerRequests + //! The session handling events to invoke server provider handle the work on server side + class ISessionHandlingServerRequests + { + public: + // Handle the destroy session process + virtual void HandleDestroySession() = 0; + + // Validate the player join session process + // @param playerConnectionConfig The required properties to validate the player join session process + // @return The result of player join session validation + virtual bool ValidatePlayerJoinSession(const PlayerConnectionConfig& playerConnectionConfig) = 0; + + // Handle the player leave session process + // @param playerConnectionConfig The required properties to handle the player leave session process + virtual void HandlePlayerLeaveSession(const PlayerConnectionConfig& playerConnectionConfig) = 0; + }; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.cpp b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.cpp new file mode 100644 index 0000000000..4eb42ab815 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.cpp @@ -0,0 +1,130 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include + +namespace AzFramework +{ + void CreateSessionRequest::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("creatorId", &CreateSessionRequest::m_creatorId) + ->Field("sessionProperties", &CreateSessionRequest::m_sessionProperties) + ->Field("sessionName", &CreateSessionRequest::m_sessionName) + ->Field("maxPlayer", &CreateSessionRequest::m_maxPlayer) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("CreateSessionRequest", "The container for CreateSession request parameters") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_creatorId, + "CreatorId", "A unique identifier for a player or entity creating the session") + ->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionProperties, + "SessionProperties", "A collection of custom properties for a session") + ->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionName, + "SessionName", "A descriptive label that is associated with a session") + ->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_maxPlayer, + "MaxPlayer", "The maximum number of players that can be connected simultaneously to the session") + ; + } + } + } + + void SearchSessionsRequest::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("filterExpression", &SearchSessionsRequest::m_filterExpression) + ->Field("sortExpression", &SearchSessionsRequest::m_sortExpression) + ->Field("maxResult", &SearchSessionsRequest::m_maxResult) + ->Field("nextToken", &SearchSessionsRequest::m_nextToken) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("SearchSessionsRequest", "The container for SearchSessions request parameters") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_filterExpression, + "FilterExpression", "String containing the search criteria for the session search") + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_sortExpression, + "SortExpression", "Instructions on how to sort the search results") + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_maxResult, + "MaxResult", "The maximum number of results to return") + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_nextToken, + "NextToken", "A token that indicates the start of the next sequential page of results") + ; + } + } + } + + void SearchSessionsResponse::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("sessionConfigs", &SearchSessionsResponse::m_sessionConfigs) + ->Field("nextToken", &SearchSessionsResponse::m_nextToken) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("SearchSessionsResponse", "The container for SearchSession request results") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_sessionConfigs, + "SessionConfigs", "A collection of sessions that match the search criteria and sorted in specific order") + ->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_nextToken, + "NextToken", "A token that indicates the start of the next sequential page of results") + ; + } + } + } + + void JoinSessionRequest::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("sessionId", &JoinSessionRequest::m_sessionId) + ->Field("playerId", &JoinSessionRequest::m_playerId) + ->Field("playerData", &JoinSessionRequest::m_playerData) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("JoinSessionRequest", "The container for JoinSession request parameters") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_sessionId, + "SessionId", "A unique identifier for the session") + ->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerId, + "PlayerId", "A unique identifier for a player. Player IDs are developer-defined") + ->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerData, + "PlayerData", "Developer-defined information related to a player") + ; + } + } + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h new file mode 100644 index 0000000000..9d21a7f282 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h @@ -0,0 +1,192 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace AzFramework +{ + struct SessionConfig; + + //! CreateSessionRequest + //! The container for CreateSession request parameters. + struct CreateSessionRequest + { + AZ_RTTI(CreateSessionRequest, "{E39C2A45-89C9-4CFB-B337-9734DC798930}"); + static void Reflect(AZ::ReflectContext* context); + + CreateSessionRequest() = default; + virtual ~CreateSessionRequest() = default; + + // A unique identifier for a player or entity creating the session. + AZStd::string m_creatorId; + + // A collection of custom properties for a session. + AZStd::unordered_map m_sessionProperties; + + // A descriptive label that is associated with a session. + AZStd::string m_sessionName; + + // The maximum number of players that can be connected simultaneously to the session. + uint64_t m_maxPlayer; + }; + + //! SearchSessionsRequest + //! The container for SearchSessions request parameters. + struct SearchSessionsRequest + { + AZ_RTTI(SearchSessionsRequest, "{B49207A8-8549-4ADB-B7D9-D7A4932F9B4B}"); + static void Reflect(AZ::ReflectContext* context); + + SearchSessionsRequest() = default; + virtual ~SearchSessionsRequest() = default; + + // String containing the search criteria for the session search. If no filter expression is included, the request returns results + // for all active sessions. + AZStd::string m_filterExpression; + + // Instructions on how to sort the search results. If no sort expression is included, the request returns results in random order. + AZStd::string m_sortExpression; + + // The maximum number of results to return. + uint8_t m_maxResult; + + // A token that indicates the start of the next sequential page of results. + AZStd::string m_nextToken; + }; + + //! SearchSessionsResponse + //! The container for SearchSession request results. + struct SearchSessionsResponse + { + AZ_RTTI(SearchSessionsResponse, "{F93DE7DC-D381-4E08-8A3B-0B08F7C38714}"); + static void Reflect(AZ::ReflectContext* context); + + SearchSessionsResponse() = default; + virtual ~SearchSessionsResponse() = default; + + // A collection of sessions that match the search criteria and sorted in specific order. + AZStd::vector m_sessionConfigs; + + // A token that indicates the start of the next sequential page of results. + AZStd::string m_nextToken; + }; + + //! JoinSessionRequest + //! The container for JoinSession request parameters. + struct JoinSessionRequest + { + AZ_RTTI(JoinSessionRequest, "{519769E8-3CDE-4385-A0D7-24DBB3685657}"); + static void Reflect(AZ::ReflectContext* context); + + JoinSessionRequest() = default; + virtual ~JoinSessionRequest() = default; + + // A unique identifier for the session. + AZStd::string m_sessionId; + + // A unique identifier for a player. Player IDs are developer-defined. + AZStd::string m_playerId; + + // Developer-defined information related to a player. + AZStd::string m_playerData; + }; + + //! ISessionRequests + //! Pure virtual session interface class to abstract the details of session handling from application code. + class ISessionRequests + { + public: + AZ_RTTI(ISessionRequests, "{D6C41A71-DD8D-47FE-8515-FAF90670AE2F}"); + + ISessionRequests() = default; + virtual ~ISessionRequests() = default; + + // Create a session for players to find and join. + // @param createSessionRequest The request of CreateSession operation + // @return The request id if session creation request succeeds; empty if it fails + virtual AZStd::string CreateSession(const CreateSessionRequest& createSessionRequest) = 0; + + // Retrieve all active sessions that match the given search criteria and sorted in specific order. + // @param searchSessionsRequest The request of SearchSessions operation + // @return The response of SearchSessions operation + virtual SearchSessionsResponse SearchSessions(const SearchSessionsRequest& searchSessionsRequest) const = 0; + + // Reserve an open player slot in a session, and perform connection from client to server. + // @param joinSessionRequest The request of JoinSession operation + // @return True if joining session succeeds; False otherwise + virtual bool JoinSession(const JoinSessionRequest& joinSessionRequest) = 0; + + // Disconnect player from session. + virtual void LeaveSession() = 0; + }; + + //! ISessionAsyncRequests + //! Async version of ISessionRequests + class ISessionAsyncRequests + { + public: + AZ_RTTI(ISessionAsyncRequests, "{471542AF-96B9-4930-82FE-242A4E68432D}"); + + ISessionAsyncRequests() = default; + virtual ~ISessionAsyncRequests() = default; + + // CreateSession Async + // @param createSessionRequest The request of CreateSession operation + virtual void CreateSessionAsync(const CreateSessionRequest& createSessionRequest) = 0; + + // SearchSessions Async + // @param searchSessionsRequest The request of SearchSessions operation + virtual void SearchSessionsAsync(const SearchSessionsRequest& searchSessionsRequest) const = 0; + + // JoinSession Async + // @param joinSessionRequest The request of JoinSession operation + virtual void JoinSessionAsync(const JoinSessionRequest& joinSessionRequest) = 0; + + // LeaveSession Async + virtual void LeaveSessionAsync() = 0; + }; + + //! SessionAsyncRequestNotifications + //! The notifications correspond to session async requests + class SessionAsyncRequestNotifications + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + // OnCreateSessionAsyncComplete is fired once CreateSessionAsync completes + // @param createSessionResponse The request id if session creation request succeeds; empty if it fails + virtual void OnCreateSessionAsyncComplete(const AZStd::string& createSessionReponse) = 0; + + // OnSearchSessionsAsyncComplete is fired once SearchSessionsAsync completes + // @param searchSessionsResponse The response of SearchSessions call + virtual void OnSearchSessionsAsyncComplete(const SearchSessionsResponse& searchSessionsResponse) = 0; + + // OnJoinSessionAsyncComplete is fired once JoinSessionAsync completes + // @param joinSessionsResponse True if joining session succeeds; False otherwise + virtual void OnJoinSessionAsyncComplete(bool joinSessionsResponse) = 0; + + // OnLeaveSessionAsyncComplete is fired once LeaveSessionAsync completes + virtual void OnLeaveSessionAsyncComplete() = 0; + }; + using SessionAsyncRequestNotificationBus = AZ::EBus; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp new file mode 100644 index 0000000000..12c5163031 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.cpp @@ -0,0 +1,74 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include + +namespace AzFramework +{ + void SessionConfig::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("creationTime", &SessionConfig::m_creationTime) + ->Field("terminationTime", &SessionConfig::m_terminationTime) + ->Field("creatorId", &SessionConfig::m_creatorId) + ->Field("sessionProperties", &SessionConfig::m_sessionProperties) + ->Field("sessionId", &SessionConfig::m_sessionId) + ->Field("sessionName", &SessionConfig::m_sessionName) + ->Field("dnsName", &SessionConfig::m_dnsName) + ->Field("ipAddress", &SessionConfig::m_ipAddress) + ->Field("port", &SessionConfig::m_port) + ->Field("maxPlayer", &SessionConfig::m_maxPlayer) + ->Field("currentPlayer", &SessionConfig::m_currentPlayer) + ->Field("status", &SessionConfig::m_status) + ->Field("statusReason", &SessionConfig::m_statusReason) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("SessionConfig", "Properties describing a session") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creationTime, + "CreationTime", "A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_terminationTime, + "TerminationTime", "A time stamp indicating when this data object was terminated. Same format as creation time.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creatorId, + "CreatorId", "A unique identifier for a player or entity creating the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionProperties, + "SessionProperties", "A collection of custom properties for a session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionId, + "SessionId", "A unique identifier for the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionName, + "SessionName", "A descriptive label that is associated with a session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_dnsName, + "DnsName", "The DNS identifier assigned to the instance that is running the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_ipAddress, + "IpAddress", "The IP address of the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_port, + "Port", "The port number for the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_maxPlayer, + "MaxPlayer", "The maximum number of players that can be connected simultaneously to the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_currentPlayer, + "CurrentPlayer", "Number of players currently in the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_status, + "Status", "Current status of the session.") + ->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_statusReason, + "StatusReason", "Provides additional information about session status."); + } + } + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h new file mode 100644 index 0000000000..22d1c9e875 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionConfig.h @@ -0,0 +1,70 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include +#include +#include + +namespace AzFramework +{ + //! SessionConfig + //! Properties describing a session. + struct SessionConfig + { + AZ_RTTI(SessionConfig, "{992DD4BE-8BA5-4071-8818-B99FD2952086}"); + static void Reflect(AZ::ReflectContext* context); + + SessionConfig() = default; + virtual ~SessionConfig() = default; + + // A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds. + uint64_t m_creationTime; + + // A time stamp indicating when this data object was terminated. Same format as creation time. + uint64_t m_terminationTime; + + // A unique identifier for a player or entity creating the session. + AZStd::string m_creatorId; + + // A collection of custom properties for a session. + AZStd::unordered_map m_sessionProperties; + + // A unique identifier for the session. + AZStd::string m_sessionId; + + // A descriptive label that is associated with a session. + AZStd::string m_sessionName; + + // The DNS identifier assigned to the instance that is running the session. + AZStd::string m_dnsName; + + // The IP address of the session. + AZStd::string m_ipAddress; + + // The port number for the session. + uint16_t m_port; + + // The maximum number of players that can be connected simultaneously to the session. + uint64_t m_maxPlayer; + + // Number of players currently in the session. + uint64_t m_currentPlayer; + + // Current status of the session. + AZStd::string m_status; + + // Provides additional information about session status. + AZStd::string m_statusReason; + }; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h new file mode 100644 index 0000000000..a61c995db7 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h @@ -0,0 +1,47 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +namespace AzFramework +{ + struct SessionConfig; + + //! SessionNotifications + //! The session notifications to listen for performing required operations + class SessionNotifications + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + + // OnSessionHealthCheck is fired in health check process + // @return The result of all OnSessionHealthCheck + virtual bool OnSessionHealthCheck() = 0; + + // OnCreateSessionBegin is fired at the beginning of session creation + // @param sessionConfig The properties to describe a session + // @return The result of all OnCreateSessionBegin notifications + virtual bool OnCreateSessionBegin(const SessionConfig& sessionConfig) = 0; + + // OnDestroySessionBegin is fired at the beginning of session termination + // @return The result of all OnDestroySessionBegin notifications + virtual bool OnDestroySessionBegin() = 0; + }; + using SessionNotificationBus = AZ::EBus; +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 6799ea9353..8045766686 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -215,6 +216,13 @@ namespace AzFramework return clone; } + AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate, + EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext) + { + return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs( + &entityTemplate, templateToCloneEntityIdMap, &serializeContext); + } + bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext) { Ticket& ticket = GetTicketPayload(*request.m_ticket); @@ -230,48 +238,37 @@ namespace AzFramework const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities(); size_t entitiesToSpawnSize = entitiesToSpawn.size(); + // Map keeps track of ids from template (spawnable) to clone (instance) + // Allowing patch ups of fields referring to entityIds outside of a given entity + EntityIdMap templateToCloneEntityIdMap; + // Reserve buffers spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize); - ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesToSpawnSize); + spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize); + templateToCloneEntityIdMap.reserve(entitiesToSpawnSize); - // TEMP: To be replaced by IdUtils::Remapper - using EntityIdMap = AZStd::unordered_map; - EntityIdMap templateToCloneIdMap; - // \TEMP - - // Clone the entities from Spawnable + // Mark all indices as spawned for (size_t i = 0; i < entitiesToSpawnSize; ++i) { const AZ::Entity& entityTemplate = *entitiesToSpawn[i]; - AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate); + AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext); + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); - clone->SetId(AZ::Entity::MakeId()); - spawnedEntities.push_back(clone); + spawnedEntities.emplace_back(clone); spawnedEntityIndices.push_back(i); + } - // TEMP: To be replaced by IdUtils::Remapper - templateToCloneIdMap[entityTemplate.GetId()] = clone->GetId(); - - // Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here. - auto* transformComponent = clone->FindComponent(); - AZ::EntityId parentId = transformComponent->GetParentId(); - if (parentId.IsValid()) - { - auto it = templateToCloneIdMap.find(parentId); - if (it != templateToCloneIdMap.end()) - { - transformComponent->SetParentRelative(it->second); - } - else - { - AZ_Warning( - "SpawnableEntitiesManager", false, "Entity %s doesn't have the parent entity %s present in the spawnable", - clone->GetName().c_str(), parentId.ToString().data()); - } - } - // \TEMP + // loadAll is true if every entity has been spawned only once + if (spawnedEntities.size() == entitiesToSpawnSize) + { + ticket.m_loadAll = true; + } + else + { + // Case where there were already spawns from a previous request + ticket.m_loadAll = false; } // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. @@ -437,10 +434,23 @@ namespace AzFramework // to load every, simply start over. ticket.m_spawnedEntityIndices.clear(); - size_t entitiesSize = entities.size(); - for (size_t i = 0; i < entitiesSize; ++i) + size_t entitiesToSpawnSize = entities.size(); + + // Map keeps track of ids from template (spawnable) to clone (instance) + // Allowing patch ups of fields referring to entityIds outside of a given entity + EntityIdMap templateToCloneEntityIdMap; + templateToCloneEntityIdMap.reserve(entitiesToSpawnSize); + + // Mark all indices as spawned + for (size_t i = 0; i < entitiesToSpawnSize; ++i) { - ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext)); + const AZ::Entity& entityTemplate = *entities[i]; + + AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext); + + AZ_Assert(clone != nullptr, "Failed to clone spawnable entity."); + + ticket.m_spawnedEntities.emplace_back(clone); ticket.m_spawnedEntityIndices.push_back(i); } } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 54f48055fd..e20f58ac76 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -29,6 +29,8 @@ namespace AZ namespace AzFramework { + using EntityIdMap = AZStd::unordered_map; + class SpawnableEntitiesManager : public SpawnableEntitiesInterface::Registrar { @@ -142,7 +144,11 @@ namespace AzFramework using Requests = AZStd::variant; - AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext); + AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, + AZ::SerializeContext& serializeContext); + + AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate, + EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext); bool ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext); bool ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext); diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index 8cff479ec8..13dff43f68 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -188,6 +188,12 @@ set(FILES Script/ScriptDebugMsgReflection.h Script/ScriptRemoteDebugging.cpp Script/ScriptRemoteDebugging.h + Session/ISessionHandlingRequests.h + Session/ISessionRequests.cpp + Session/ISessionRequests.h + Session/SessionConfig.cpp + Session/SessionConfig.h + Session/SessionNotifications.h StreamingInstall/StreamingInstall.h StreamingInstall/StreamingInstall.cpp StreamingInstall/StreamingInstallRequests.h diff --git a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp index 78020cb09d..70000dc9a8 100644 --- a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp @@ -116,8 +116,8 @@ namespace AzNetworking int32_t TcpSocket::Receive(uint8_t* outData, uint32_t size) const { - AZ_Assert(size > 0, "Invalid data size for send"); - AZ_Assert(outData != nullptr, "NULL data pointer passed to send"); + AZ_Assert(size > 0, "Invalid data size for receive"); + AZ_Assert(outData != nullptr, "NULL data pointer passed to receive"); if (!IsOpen()) { return SocketOpResultErrorNotOpen; @@ -176,7 +176,7 @@ namespace AzNetworking if (::bind(aznumeric_cast(m_socketFd), (const sockaddr*)&hints, sizeof(hints)) != 0) { const int32_t error = GetLastNetworkError(); - AZLOG_ERROR("Failed to bind socket (%d:%s)", error, GetNetworkErrorDesc(error)); + AZLOG_ERROR("Failed to bind TCP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); return false; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp index 870545e6c8..5676d48150 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp @@ -162,7 +162,7 @@ namespace AzNetworking const UdpReaderThread::ReceivedPackets* packets = m_readerThread.GetReceivedPackets(m_socket.get()); if (packets == nullptr) { - AZ_Assert(false, "nullptr was retrieved for the received packet buffer, check that the socket has been registered with the reader thread"); + // Socket is not yet registered with the reader thread and is likely still pending, try again later return; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp index e642c87623..cbb5f8e6c0 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp @@ -82,7 +82,7 @@ namespace AzNetworking if (::bind(static_cast(m_socketFd), (const sockaddr *)&hints, sizeof(hints)) != 0) { const int32_t error = GetLastNetworkError(); - AZLOG_ERROR("Failed to bind socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); + AZLOG_ERROR("Failed to bind UDP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error)); return false; } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h index 19c236f509..8412361657 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include #include @@ -46,6 +47,10 @@ namespace AzToolsFramework virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0; + //! Get all Assets generated by Prefab processing when entering Play-In Editor mode (Ctrl+G) + //! /return The vector of Assets generated by Prefab processing + virtual const AZStd::vector>& GetPlayInEditorAssetData() = 0; + virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0; virtual bool SaveToStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 02243a8c77..da529d9349 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -314,6 +314,11 @@ namespace AzToolsFramework return *m_rootInstance; } + const AZStd::vector>& PrefabEditorEntityOwnershipService::GetPlayInEditorAssetData() + { + return m_playInEditorData.m_assets; + } + void PrefabEditorEntityOwnershipService::OnEntityRemoved(AZ::EntityId entityId) { AzFramework::SliceEntityRequestBus::MultiHandler::BusDisconnect(entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h index 48e07df091..3be9b95df0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h @@ -195,6 +195,8 @@ namespace AzToolsFramework AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override; Prefab::InstanceOptionalReference GetRootPrefabInstance() override; + + const AZStd::vector>& GetPlayInEditorAssetData() override; ////////////////////////////////////////////////////////////////////////// void OnEntityRemoved(AZ::EntityId entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp index 8c371baf8c..890253b528 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.cpp @@ -27,6 +27,7 @@ AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option") // 4244: con #include #include #include +#include AZ_POP_DISABLE_WARNING static const int LabelColumnStretch = 2; @@ -121,6 +122,19 @@ namespace AzToolsFramework setLayout(m_mainLayout); } + void PropertyRowWidget::paintEvent(QPaintEvent* event) + { + QStylePainter p(this); + + if (CanBeReordered()) + { + const QPen linePen(QColor(0x3B3E3F)); + p.setPen(linePen); + int indent = m_treeDepth * m_treeIndentation; + p.drawLine(event->rect().topLeft() + QPoint(indent, 0), event->rect().topRight()); + } + } + bool PropertyRowWidget::HasChildWidgetAlready() const { return m_childWidget != nullptr; @@ -1661,6 +1675,20 @@ namespace AzToolsFramework m_nameLabel->setFilter(m_currentFilterString); } + bool PropertyRowWidget::CanChildrenBeReordered() const + { + return m_containerEditable; + } + + bool PropertyRowWidget::CanBeReordered() const + { + if (!m_parentRow) + { + return false; + } + + return m_parentRow->CanChildrenBeReordered(); + } } #include "UI/PropertyEditor/moc_PropertyRowWidget.cpp" diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx index e4b538ccdc..79121403c9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyRowWidget.hxx @@ -44,6 +44,7 @@ namespace AzToolsFramework Q_PROPERTY(bool hasChildRows READ HasChildRows); Q_PROPERTY(bool isTopLevel READ IsTopLevel); Q_PROPERTY(int getLevel READ GetLevel); + Q_PROPERTY(bool canBeReordered READ CanBeReordered); Q_PROPERTY(bool appendDefaultLabelToName READ GetAppendDefaultLabelToName WRITE AppendDefaultLabelToName) public: AZ_CLASS_ALLOCATOR(PropertyRowWidget, AZ::SystemAllocator, 0) @@ -126,6 +127,7 @@ namespace AzToolsFramework void SetSelectionEnabled(bool selectionEnabled); void SetSelected(bool selected); bool eventFilter(QObject *watched, QEvent *event) override; + void paintEvent(QPaintEvent*) override; /// Apply tooltip to widget and some of its children. void SetDescription(const QString& text); @@ -146,6 +148,9 @@ namespace AzToolsFramework QLabel* GetNameLabel() { return m_nameLabel; } void SetIndentSize(int w); void SetAsCustom(bool custom) { m_custom = custom; } + + bool CanChildrenBeReordered() const; + bool CanBeReordered() const; protected: int CalculateLabelWidth() const; diff --git a/Code/Sandbox/Editor/Style/Editor.qss b/Code/Sandbox/Editor/Style/Editor.qss index 0c3f64b85c..fa7d67dd43 100644 --- a/Code/Sandbox/Editor/Style/Editor.qss +++ b/Code/Sandbox/Editor/Style/Editor.qss @@ -38,6 +38,11 @@ AzToolsFramework--ComponentPaletteWidget > QTreeView background-color: #222222; } +AzToolsFramework--PropertyRowWidget[canBeReordered="true"] QLabel#Name +{ + font-weight: bold; +} + /* Style for visualizing property values overridden from their prefab values */ AzToolsFramework--PropertyRowWidget[IsOverridden=true] #Name QLabel, AzToolsFramework--ComponentEditorHeader #Title[IsOverridden="true"] diff --git a/Code/Tools/ProjectManager/Resources/Backgrounds/FirstTimeBackgroundImage.jpg b/Code/Tools/ProjectManager/Resources/Backgrounds/FirstTimeBackgroundImage.jpg new file mode 100644 index 0000000000..bfa5f83cf6 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/Backgrounds/FirstTimeBackgroundImage.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7088e902885d98953f6a1715efab319c063a4ab8918fd0e810251c8ed82b8514 +size 542983 diff --git a/Code/Tools/ProjectManager/Resources/Backgrounds/LICENSE.TXT b/Code/Tools/ProjectManager/Resources/Backgrounds/LICENSE.TXT new file mode 100644 index 0000000000..5adf81a289 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/Backgrounds/LICENSE.TXT @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, +SPDX-FileCopyrightText: modify, distribute, perform, and use photos from Unsplash for free, including for commercial +SPDX-FileCopyrightText: purposes, without permission from or attributing the photographer or Unsplash. This license does +SPDX-FileCopyrightText: not include the right to compile photos from Unsplash to replicate a similar or competing service. \ No newline at end of file diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp index aa4dee1aa6..2c96078d43 100644 --- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp +++ b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp @@ -12,18 +12,64 @@ #include -#include +#include +#include +#include +#include +#include +#include namespace O3DE::ProjectManager { + inline constexpr static int s_contentMargins = 80; + inline constexpr static int s_buttonSpacing = 30; + inline constexpr static int s_iconSize = 24; + inline constexpr static int s_spacerSize = 20; + inline constexpr static int s_boxButtonWidth = 210; + inline constexpr static int s_boxButtonHeight = 280; + FirstTimeUseScreen::FirstTimeUseScreen(QWidget* parent) : ScreenWidget(parent) - , m_ui(new Ui::FirstTimeUseClass()) { - m_ui->setupUi(this); + QVBoxLayout* vLayout = new QVBoxLayout(); + setLayout(vLayout); + vLayout->setContentsMargins(s_contentMargins, s_contentMargins, s_contentMargins, s_contentMargins); - connect(m_ui->createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton); - connect(m_ui->openProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleOpenProjectButton); + QLabel* titleLabel = new QLabel(this); + titleLabel->setText(tr("Ready. Set. Create!")); + titleLabel->setStyleSheet("font-size: 60px"); + vLayout->addWidget(titleLabel); + + QLabel* introLabel = new QLabel(this); + introLabel->setTextFormat(Qt::AutoText); + introLabel->setText(tr("

Welcome to O3DE! Start something new by creating a project. Not sure what to create?

Explore what\342\200\231s available by downloading our sample project.

")); + introLabel->setStyleSheet("font-size: 14px"); + vLayout->addWidget(introLabel); + + QHBoxLayout* buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(s_buttonSpacing); + + m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this); + m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize)); + buttonLayout->addWidget(m_createProjectButton); + + m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this); + m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize)); + buttonLayout->addWidget(m_addProjectButton); + + QSpacerItem* buttonSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Expanding, QSizePolicy::Minimum); + buttonLayout->addItem(buttonSpacer); + + vLayout->addItem(buttonLayout); + + QSpacerItem* verticalSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Minimum, QSizePolicy::Expanding); + vLayout->addItem(verticalSpacer); + + // Using border-image allows for scaling options background-image does not support + setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }"); + + connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton); + connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton); } ProjectManagerScreen FirstTimeUseScreen::GetScreenEnum() @@ -36,9 +82,21 @@ namespace O3DE::ProjectManager emit ResetScreenRequest(ProjectManagerScreen::NewProjectSettingsCore); emit ChangeScreenRequest(ProjectManagerScreen::NewProjectSettingsCore); } - void FirstTimeUseScreen::HandleOpenProjectButton() + void FirstTimeUseScreen::HandleAddProjectButton() { emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome); } + QPushButton* FirstTimeUseScreen::CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent) + { + QPushButton* largeBoxButton = new QPushButton(icon, text, parent); + + largeBoxButton->setFixedSize(s_boxButtonWidth, s_boxButtonHeight); + largeBoxButton->setFlat(true); + largeBoxButton->setFocusPolicy(Qt::FocusPolicy::NoFocus); + largeBoxButton->setStyleSheet("QPushButton { font-size: 14px; background-color: rgba(0, 0, 0, 191); }"); + + return largeBoxButton; + } + } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h index 4b4a99f16a..b6b57dc16b 100644 --- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h +++ b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.h @@ -15,10 +15,8 @@ #include #endif -namespace Ui -{ - class FirstTimeUseClass; -} +QT_FORWARD_DECLARE_CLASS(QIcon) +QT_FORWARD_DECLARE_CLASS(QPushButton) namespace O3DE::ProjectManager { @@ -32,10 +30,13 @@ namespace O3DE::ProjectManager protected slots: void HandleNewProjectButton(); - void HandleOpenProjectButton(); + void HandleAddProjectButton(); private: - QScopedPointer m_ui; + QPushButton* CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr); + + QPushButton* m_createProjectButton; + QPushButton* m_addProjectButton; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui deleted file mode 100644 index fdc195731f..0000000000 --- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.ui +++ /dev/null @@ -1,93 +0,0 @@ - - - FirstTimeUseClass - - - - 0 - 0 - 881 - 555 - - - - Form - - - - - - - - - 30 - - - - READY. SET. CREATE! - - - - - - - <html><head/><body><p>Welcome to O3DE! Start something new by creating a project. Not sure what to create? </p><p>Explore what’s available by downloading our sample project.</p></body></html> - - - Qt::AutoText - - - - - - - - - - - - 0 - 0 - - - - Create Project - - - - :/Resources/Add.svg:/Resources/Add.svg - - - - 16 - 16 - - - - - - - - - 0 - 0 - - - - Open a Project - - - - :/Resources/Select_Folder.svg:/Resources/Select_Folder.svg - - - - - - - - - - - - diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index 977667071f..6b9d268564 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -27,6 +27,12 @@ namespace O3DE::ProjectManager , m_ui(new Ui::ProjectManagerWindowClass()) { m_ui->setupUi(this); + QLayout* layout = m_ui->centralWidget->layout(); + layout->setMargin(0); + layout->setSpacing(0); + layout->setContentsMargins(0, 0, 0, 0); + + setFixedSize(this->geometry().width(), this->geometry().height()); m_pythonBindings = AZStd::make_unique(engineRootPath); diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui index 789dd1b656..a71ed3aabf 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui @@ -6,10 +6,16 @@ 0 0 - 800 - 600 + 1200 + 800 + + + 0 + 0 + + O3DE Project Manager @@ -21,7 +27,7 @@ 0 0 - 800 + 1200 36 diff --git a/Code/Tools/ProjectManager/Source/ScreenWidget.h b/Code/Tools/ProjectManager/Source/ScreenWidget.h index ae235daf2b..483066e031 100644 --- a/Code/Tools/ProjectManager/Source/ScreenWidget.h +++ b/Code/Tools/ProjectManager/Source/ScreenWidget.h @@ -15,18 +15,20 @@ #include #include +#include +#include #endif namespace O3DE::ProjectManager { class ScreenWidget - : public QWidget + : public QFrame { Q_OBJECT public: explicit ScreenWidget(QWidget* parent = nullptr) - : QWidget(parent) + : QFrame(parent) { } ~ScreenWidget() = default; diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index 69af09f496..b8a38ed155 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -22,6 +22,9 @@ namespace O3DE::ProjectManager : QWidget(parent) { QVBoxLayout* vLayout = new QVBoxLayout(); + vLayout->setMargin(0); + vLayout->setSpacing(0); + vLayout->setContentsMargins(0, 0, 0, 0); setLayout(vLayout); m_screenStack = new QStackedWidget(); diff --git a/Code/Tools/ProjectManager/project_manager.qrc b/Code/Tools/ProjectManager/project_manager.qrc index 6509a9f940..3c23bc24ff 100644 --- a/Code/Tools/ProjectManager/project_manager.qrc +++ b/Code/Tools/ProjectManager/project_manager.qrc @@ -9,5 +9,6 @@ Resources/iOS.svg Resources/Linux.svg Resources/macOS.svg + Resources/Backgrounds/FirstTimeBackgroundImage.jpg diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 9ffdb6029d..3594d1e079 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -22,7 +22,6 @@ set(FILES Source/EngineInfo.cpp Source/FirstTimeUseScreen.h Source/FirstTimeUseScreen.cpp - Source/FirstTimeUseScreen.ui Source/ProjectManagerWindow.h Source/ProjectManagerWindow.cpp Source/ProjectTemplateInfo.h diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp index 0ee25195bc..38f7de89c6 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp @@ -260,7 +260,7 @@ namespace AZ { AZ_TraceContext("Importer", "Animation"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); // Add check for animation layers at the scene level. @@ -387,11 +387,10 @@ namespace AZ } Events::ProcessingResultCombiner combinedAnimationResult; - for (AZ::u32 meshIndex = 0; meshIndex < currentNode->mNumMeshes; ++meshIndex) + if (context.m_sourceNode.ContainsMesh()) { - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[meshIndex]]; - - if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(mesh->mName.C_Str()); + const aiMesh* firstMesh = scene->mMeshes[currentNode->mMeshes[0]]; + if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(firstMesh->mName.C_Str()); channelsForMeshName != meshMorphAnimations.end()) { const auto [nodeIterName, channels] = *channelsForMeshName; @@ -399,7 +398,7 @@ namespace AZ { const auto& [animation, morphAnimation] = animAndMorphAnim; combinedAnimationResult += ImportBlendShapeAnimation( - context, animation, morphAnimation, mesh); + context, animation, morphAnimation, firstMesh); } } } @@ -413,32 +412,39 @@ namespace AZ if (boneAnimations.empty() && !meshMorphAnimations.empty()) { const aiAnimation* animation = scene->mAnimations[0]; - - // Morph animations need a regular animation on the node, as well. - // If there is no bone animation on the current node, then generate one here. - AZStd::shared_ptr createdAnimationData = - AZStd::make_shared(); - - const size_t numKeyframes = animation->mDuration + 1; // +1 because we start at 0 and the last keyframe is at mDuration instead of mDuration-1 - createdAnimationData->ReserveKeyFrames(numKeyframes); - - const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond; - createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames); - - // Set every frame of the animation to the start location of the node. - aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode); - DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); - context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); - context.m_sourceSceneSystem.ConvertUnit(localTransform); - for (AZ::u32 time = 0; time <= animation->mDuration; ++time) + for (AZ::u32 channelIndex = 0; channelIndex < animation->mNumMorphMeshChannels; ++channelIndex) { - createdAnimationData->AddKeyFrame(localTransform); + const aiMeshMorphAnim* nodeAnim = animation->mMorphMeshChannels[channelIndex]; + // Morph animations need a regular animation on the node, as well. + // If there is no bone animation on the current node, then generate one here. + AZStd::shared_ptr createdAnimationData = + AZStd::make_shared(); + + const size_t numKeyframes = GetNumKeyFrames( + nodeAnim->mNumKeys, + animation->mDuration, + animation->mTicksPerSecond); + createdAnimationData->ReserveKeyFrames(numKeyframes); + + const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond; + createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames); + + // Set every frame of the animation to the start location of the node. + aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode); + DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); + context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); + context.m_sourceSceneSystem.ConvertUnit(localTransform); + for (AZ::u32 time = 0; time <= numKeyframes; ++time) + { + createdAnimationData->AddKeyFrame(localTransform); + } + + const AZStd::string stubBoneAnimForMorphName(AZStd::string::format("%s%s", nodeName.c_str(), nodeAnim->mName.C_Str())); + Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild( + context.m_currentGraphPosition, stubBoneAnimForMorphName.c_str(), AZStd::move(createdAnimationData)); + context.m_scene.GetGraph().MakeEndPoint(addNode); } - - Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild( - context.m_currentGraphPosition, nodeName.c_str(), AZStd::move(createdAnimationData)); - context.m_scene.GetGraph().MakeEndPoint(addNode); - + return combinedAnimationResult.GetResult(); } decltype(boneAnimations) parentFillerAnimations; @@ -446,8 +452,8 @@ namespace AZ // Go through all the animations and make sure we create animations for bones who's parents don't have an animation for (auto&& anim : boneAnimations) { - aiNode* node = scene->mRootNode->FindNode(anim.first.c_str()); - aiNode* parent = node->mParent; + const aiNode* node = scene->mRootNode->FindNode(anim.first.c_str()); + const aiNode* parent = node->mParent; while (parent && parent != scene->mRootNode) { @@ -598,7 +604,8 @@ namespace AZ // Keyframes generated for every single frame of the animation. typedef AZStd::map> ValueToKeyDataMap; ValueToKeyDataMap valueToKeyDataMap; - + // Key time can be less than zero, normalize to have zero be the lowest time. + double keyOffset = 0; for (int keyIdx = 0; keyIdx < meshMorphAnim->mNumKeys; keyIdx++) { aiMeshMorphKey& key = meshMorphAnim->mKeys[keyIdx]; @@ -609,6 +616,10 @@ namespace AZ valueToKeyDataMap[currentValue].insert( AZStd::upper_bound(valueToKeyDataMap[currentValue].begin(), valueToKeyDataMap[currentValue].end(),thisKey), thisKey); + if (key.mTime < keyOffset) + { + keyOffset = key.mTime; + } } } @@ -631,7 +642,7 @@ namespace AZ const double time = GetTimeForFrame(frame, animation->mTicksPerSecond); float weight = 0; - if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx)) + if (!SampleKeyFrame(weight, keys, keys.size(), time + keyOffset, keyIdx)) { return Events::ProcessingResult::Failure; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp index c2b1f20035..2ce9bc14f4 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBitangentStreamImporter.cpp @@ -25,7 +25,6 @@ #include #include - namespace AZ { namespace SceneAPI @@ -44,7 +43,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,62 +54,79 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex) { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + return scene->mMeshes[meshIndex]->HasTangentsAndBitangents(); + }; - size_t vertexCount = parentMeshData->GetVertexCount(); - - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes) - { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct bitangent stream attribute for invalid or non-mesh parent data, mesh index is invalid"); - return Events::ProcessingResult::Failure; - } - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; - - if (!mesh->HasTangentsAndBitangents()) + // If there are no bitangents on any meshes, there's nothing to import in this function. + const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!anyMeshHasTangentsAndBitangents) { return Events::ProcessingResult::Ignored; } + // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material. + // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes, + // and the engine has code to do this later. + const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!allMeshesHaveTangentsAndBitangents) + { + const char* mixedBitangentsError = + "Node with name %s has meshes with and without bitangents. " + "Placeholder incorrect bitangents will be generated to allow the data to process, " + "but the source art needs to be fixed to correct this. Either apply bitangents to all meshes on this node, " + "or remove all bitangents from all meshes on this node."; + AZ_Error( + Utilities::ErrorWindow, false, mixedBitangentsError, currentNode->mName.C_Str()); + } + + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + AZStd::shared_ptr bitangentStream = AZStd::make_shared(); - // AssImp only has one bitangentStream per mesh. bitangentStream->SetBitangentSetIndex(0); bitangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx); bitangentStream->ReserveContainerSpace(vertexCount); - - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - const Vector3 bitangent( - AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v])); - bitangentStream->AppendBitangent(bitangent); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (!mesh->HasTangentsAndBitangents()) + { + // This node has mixed meshes with and without bitangents. + // An error was already thrown above. Output stub bitangents so + // the mesh can still be output in some form, even if the data isn't correct. + // The bitangent count needs to match the vertex count on the associated mesh node. + bitangentStream->AppendBitangent(Vector3::CreateAxisY()); + } + else + { + const Vector3 bitangent( + AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v])); + bitangentStream->AppendBitangent(bitangent); + } + } } - AZStd::string nodeName(AZStd::string::format("%s",m_defaultNodeName)); Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName); Events::ProcessingResult bitangentResults; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, nodeName.c_str()); + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, m_defaultNodeName); bitangentResults = Events::Process(dataPopulated); if (bitangentResults != Events::ProcessingResult::Failure) { bitangentResults = AddAttributeDataNodeWithContexts(dataPopulated); } - return bitangentResults; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp index c0399329d3..34266a3e29 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBlendShapeImporter.cpp @@ -74,37 +74,51 @@ namespace AZ { return meshDataResult.GetError(); } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - int parentMeshIndex = parentMeshData->GetSdkMeshIndex(); Events::ProcessingResultCombiner combinedBlendShapeResult; + // 1. Loop through meshes & anims + // Create storage: Anim to meshes + // 2. Loop through anims & meshes + // Create an anim mesh for each anim, with meshes re-combined. + // AssImp separates meshes that have multiple materials. + // This code re-combines them to match previous FBX SDK behavior, + // so they can be separated by engine code instead. + AZStd::map>> animToMeshToAnimMeshIndices; for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++) { int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx]; const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx]; - - // Each mesh gets its own node in the scene graph, so only generate - // morph targets for the current mesh. - if (parentMeshIndex != nodeMeshIdx || !aiMesh->mNumAnimMeshes) - { - continue; - } - for (int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++) { - AZStd::shared_ptr blendShapeData = - AZStd::make_shared(); - aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx]; - AZStd::string nodeName(aiAnimMesh->mName.C_Str()); - size_t dotIndex = nodeName.rfind('.'); - if (dotIndex != AZStd::string::npos) - { - nodeName.erase(0, dotIndex + 1); - } - RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape"); - AZ_TraceContext("Blend shape name", nodeName); + animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx); + } + } + + for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices) + { + AZStd::shared_ptr blendShapeData = + AZStd::make_shared(); + + // Some DCC tools, like Maya, include a full path separated by '.' in the node names. + // For example, "cone_skin_blendShapeNode.cone_squash" + // Downstream processing doesn't want anything but the last part of that node name, + // so find the last '.' and remove anything before it. + AZStd::string nodeName(animToMeshIndex.first); + size_t dotIndex = nodeName.rfind('.'); + if (dotIndex != AZStd::string::npos) + { + nodeName.erase(0, dotIndex + 1); + } + int vertexOffset = 0; + RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape"); + AZ_TraceContext("Blend shape name", nodeName); + for (const auto& meshIndex : animToMeshIndex.second) + { + int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[meshIndex.first]; + const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx]; + const aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[meshIndex.second]; AZStd::bitset uvSetUsedFlags; for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex) @@ -128,7 +142,7 @@ namespace AZ context.m_sourceSceneSystem.ConvertUnit(vertex); blendShapeData->AddPosition(vertex); - blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx); + blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx + vertexOffset, vertIdx + vertexOffset); // Add normals if (aiAnimMesh->HasNormals()) @@ -191,33 +205,36 @@ namespace AZ } for (int idx = 0; idx < face.mNumIndices; ++idx) { - blendFace.vertexIndex[idx] = face.mIndices[idx]; + blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset; } blendShapeData->AddFace(blendFace); } + vertexOffset += aiMesh->mNumVertices; - // Report problem if no vertex or face converted to MeshData - if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0) - { - AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str()); - return Events::ProcessingResult::Failure; - } - Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); - - Events::ProcessingResult blendShapeResult; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName); - blendShapeResult = Events::Process(dataPopulated); - - if (blendShapeResult != Events::ProcessingResult::Failure) - { - blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated); - } - combinedBlendShapeResult += blendShapeResult; } + + // Report problem if no vertex or face converted to MeshData + if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0) + { + AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str()); + return Events::ProcessingResult::Failure; + } + + Containers::SceneGraph::NodeIndex newIndex = + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + + Events::ProcessingResult blendShapeResult; + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName); + blendShapeResult = Events::Process(dataPopulated); + + if (blendShapeResult != Events::ProcessingResult::Failure) + { + blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated); + } + combinedBlendShapeResult += blendShapeResult; } return combinedBlendShapeResult.GetResult(); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp index 4467d6933b..5b43941715 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp @@ -46,8 +46,8 @@ namespace AZ } void EnumBonesInNode( - const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, - AZStd::unordered_map& boneLookup) + const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, + AZStd::unordered_map& boneLookup) { /* From AssImp Documentation a) Create a map or a similar container to store which nodes are necessary for the skeleton. Pre-initialise it for all nodes with a "no". @@ -62,14 +62,14 @@ namespace AZ for (unsigned meshIndex = 0; meshIndex < node->mNumMeshes; ++meshIndex) { - aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]]; + const aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]]; for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) { - aiBone* bone = mesh->mBones[boneIndex]; + const aiBone* bone = mesh->mBones[boneIndex]; - aiNode* boneNode = scene->mRootNode->FindNode(bone->mName); - aiNode* boneParent = boneNode->mParent; + const aiNode* boneNode = scene->mRootNode->FindNode(bone->mName); + const aiNode* boneParent = boneNode->mParent; mainBoneList[bone->mName.C_Str()] = boneNode; boneLookup[bone->mName.C_Str()] = bone; @@ -85,8 +85,8 @@ namespace AZ } void EnumChildren( - const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, - AZStd::unordered_map& boneLookup) + const aiScene* scene, const aiNode* node, AZStd::unordered_map& mainBoneList, + AZStd::unordered_map& boneLookup) { EnumBonesInNode(scene, node, mainBoneList, boneLookup); @@ -102,7 +102,7 @@ namespace AZ { AZ_TraceContext("Importer", "Bone"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (IsPivotNode(currentNode->mName)) @@ -118,8 +118,8 @@ namespace AZ } else { - AZStd::unordered_map mainBoneList; - AZStd::unordered_map boneLookup; + AZStd::unordered_map mainBoneList; + AZStd::unordered_map boneLookup; EnumChildren(scene, scene->mRootNode, mainBoneList, boneLookup); if (mainBoneList.find(currentNode->mName.C_Str()) != mainBoneList.end()) @@ -172,7 +172,7 @@ namespace AZ } aiMatrix4x4 transform = currentNode->mTransformation; - aiNode* parent = currentNode->mParent; + const aiNode* parent = currentNode->mParent; while (parent) { diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp index 7ebdb55363..75fa39105f 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -44,7 +45,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,43 +56,64 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) - { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + // This node has at least one mesh, verify that the color channel counts are the same for all meshes. + const int expectedColorChannels = scene->mMeshes[currentNode->mMeshes[0]]->GetNumColorChannels(); + const bool allMeshesHaveSameNumberOfColorChannels = + AZStd::all_of(currentNode->mMeshes + 1, currentNode->mMeshes + currentNode->mNumMeshes, [scene, expectedColorChannels](const unsigned int meshIndex) + { + return scene->mMeshes[meshIndex]->GetNumColorChannels() == expectedColorChannels; + }); - size_t vertexCount = parentMeshData->GetVertexCount(); + AZ_Error( + Utilities::ErrorWindow, + allMeshesHaveSameNumberOfColorChannels, + "Color channel counts for node %s has meshes with different color channel counts. " + "The color channel count for the first mesh will be used, and placeholder incorrect color values " + "will be generated to allow the data to process, but the source art needs to be fixed to correct this. " + "All meshes on this node should have the same number of color channels.", + currentNode->mName.C_Str()); - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0) + if (expectedColorChannels == 0) { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct color stream attribute for invalid or non-mesh parent data, mesh index is missing"); - return Events::ProcessingResult::Failure; + return Events::ProcessingResult::Ignored; } - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); Events::ProcessingResultCombiner combinedVertexColorResults; - for (int colorSetIndex = 0; colorSetIndex < mesh->GetNumColorChannels(); ++colorSetIndex) + for (int colorSetIndex = 0; colorSetIndex < expectedColorChannels; ++colorSetIndex) { + AZStd::shared_ptr vertexColors = AZStd::make_shared(); vertexColors->ReserveContainerSpace(vertexCount); - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - AZ::SceneAPI::DataTypes::Color vertexColor( - AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v])); - vertexColors->AppendColor(vertexColor); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (colorSetIndex < mesh->GetNumColorChannels()) + { + AZ::SceneAPI::DataTypes::Color vertexColor( + AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v])); + vertexColors->AppendColor(vertexColor); + } + else + { + // An error was already emitted if this mesh has less color channels + // than other meshes on the parent node. Append an arbitrary color value, fully opaque black, + // so the mesh can still be processed. + // It's better to let the engine load a partially valid mesh than to completely fail. + vertexColors->AppendColor(AZ::SceneAPI::DataTypes::Color(0.0f,0.0f,0.0f,1.0f)); + } + } } - AZStd::string nodeName(AZStd::string::format("%s%d",m_defaultNodeName,colorSetIndex)); + AZStd::string nodeName(AZStd::string::format("%s%d", m_defaultNodeName, colorSetIndex)); Containers::SceneGraph::NodeIndex newIndex = context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); @@ -106,9 +128,7 @@ namespace AZ combinedVertexColorResults += colorMapResults; } - return combinedVertexColorResults.GetResult(); - } } // namespace FbxSceneBuilder diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp index 80ac01ebdf..e79eaa09aa 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpImporterUtilities.cpp @@ -69,7 +69,7 @@ namespace AZ aiMatrix4x4 GetConcatenatedLocalTransform(const aiNode* currentNode) { - aiNode* parent = currentNode->mParent; + const aiNode* parent = currentNode->mParent; aiMatrix4x4 combinedTransform = currentNode->mTransformation; while (parent) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp index e314804ea1..a912b90e34 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMaterialImporter.cpp @@ -62,7 +62,7 @@ namespace AZ for (int idx = 0; idx < context.m_sourceNode.m_assImpNode->mNumMeshes; ++idx) { int meshIndex = context.m_sourceNode.m_assImpNode->mMeshes[idx]; - aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex]; + const aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex]; AZ_Assert(assImpMesh, "Asset Importer Mesh should not be null."); int materialIndex = assImpMesh->mMaterialIndex; AZ_TraceContext("Material Index", materialIndex); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp index cafb96934d..193a1f9fd5 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpMeshImporter.cpp @@ -45,7 +45,7 @@ namespace AZ { AZ_TraceContext("Importer", "Mesh"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (!context.m_sourceNode.ContainsMesh() || IsSkinnedMesh(*currentNode, *scene)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp index 145dc9a457..f4a5fd0f93 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinImporter.cpp @@ -45,7 +45,7 @@ namespace AZ { AZ_TraceContext("Importer", "Skin"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (!context.m_sourceNode.ContainsMesh() || !IsSkinnedMesh(*currentNode, *scene)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp index abcbf10b4a..d8503857cc 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.cpp @@ -51,7 +51,7 @@ namespace AZ { AZ_TraceContext("Importer", "Skin Weights"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if(currentNode->mNumMeshes <= 0) @@ -59,35 +59,21 @@ namespace AZ return Events::ProcessingResult::Ignored; } - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) - { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - - int parentMeshIndex = parentMeshData->GetSdkMeshIndex(); - Events::ProcessingResultCombiner combinedSkinWeightsResult; + // Don't create this until a bone with weights is encountered + Containers::SceneGraph::NodeIndex weightsIndexForMesh; + AZStd::string skinWeightName; + AZStd::shared_ptr skinWeightData; + + const uint64_t totalVertices = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + + int vertexCount = 0; for(unsigned nodeMeshIndex = 0; nodeMeshIndex < currentNode->mNumMeshes; ++nodeMeshIndex) { - if (nodeMeshIndex != parentMeshIndex) - { - // Only generate skinning data for the parent mesh. - // Each AssImp mesh is assigned to a unique node, - // so the skinning data should be generated as a child node - // for the associated parent mesh. - continue; - } int sceneMeshIndex = currentNode->mMeshes[nodeMeshIndex]; const aiMesh* mesh = scene->mMeshes[sceneMeshIndex]; - // Don't create this until a bone with weights is encountered - Containers::SceneGraph::NodeIndex weightsIndexForMesh; - AZStd::string skinWeightName; - AZStd::shared_ptr skinWeightData; - for(unsigned b = 0; b < mesh->mNumBones; ++b) { const aiBone* bone = mesh->mBones[b]; @@ -100,7 +86,6 @@ namespace AZ if (!weightsIndexForMesh.IsValid()) { skinWeightName = s_skinWeightName; - skinWeightName += AZStd::to_string(nodeMeshIndex); RenamedNodesMap::SanitizeNodeName(skinWeightName, context.m_scene.GetGraph(), context.m_currentGraphPosition); weightsIndexForMesh = @@ -116,23 +101,25 @@ namespace AZ } Pending pending; pending.m_bone = bone; - pending.m_numVertices = mesh->mNumVertices; + pending.m_numVertices = totalVertices; pending.m_skinWeightData = skinWeightData; + pending.m_vertOffset = vertexCount; m_pendingSkinWeights.push_back(pending); } - - Events::ProcessingResult skinWeightsResult; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName); - skinWeightsResult = Events::Process(dataPopulated); - - if (skinWeightsResult != Events::ProcessingResult::Failure) - { - skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated); - } - - combinedSkinWeightsResult += skinWeightsResult; + vertexCount += mesh->mNumVertices; } + Events::ProcessingResult skinWeightsResult; + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName); + skinWeightsResult = Events::Process(dataPopulated); + + if (skinWeightsResult != Events::ProcessingResult::Failure) + { + skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated); + } + + combinedSkinWeightsResult += skinWeightsResult; + return combinedSkinWeightsResult.GetResult(); } @@ -153,7 +140,7 @@ namespace AZ link.boneId = boneId; link.weight = it.m_bone->mWeights[weight].mWeight; - it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId, link); + it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId + it.m_vertOffset, link); } } const auto result = m_pendingSkinWeights.empty() ? Events::ProcessingResult::Ignored : Events::ProcessingResult::Success; diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h index f048fe0af2..655c838701 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpSkinWeightsImporter.h @@ -61,6 +61,7 @@ namespace AZ { const aiBone* m_bone = nullptr; unsigned m_numVertices = 0; + unsigned m_vertOffset = 0; AZStd::shared_ptr m_skinWeightData; }; diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp index 992a6a6ab1..47b7e410b4 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -44,7 +45,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(2); // LYN-2576 + serializeContext->Class()->Version(3); // LYN-3250 } } @@ -55,62 +56,79 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + + const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex) { - return meshDataResult.GetError(); - } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); + return scene->mMeshes[meshIndex]->HasTangentsAndBitangents(); + }; - size_t vertexCount = parentMeshData->GetVertexCount(); - - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes) - { - AZ_Error(Utilities::ErrorWindow, false, - "Tried to construct tangent stream attribute for invalid or non-mesh parent data, mesh index is invalid"); - return Events::ProcessingResult::Failure; - } - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; - - if (!mesh->HasTangentsAndBitangents()) + // If there are no tangents on any meshes, there's nothing to import in this function. + const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!anyMeshHasTangentsAndBitangents) { return Events::ProcessingResult::Ignored; } + // AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material. + // This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes, + // and the engine has code to do this later. + const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents); + if (!allMeshesHaveTangentsAndBitangents) + { + const char* mixedTangentsError = + "Node with name %s has meshes with and without tangents. " + "Placeholder incorrect tangents will be generated to allow the data to process, " + "but the source art needs to be fixed to correct this. Either apply tangents to all meshes on this node, " + "or remove all tangents from all meshes on this node."; + AZ_Error( + Utilities::ErrorWindow, false, mixedTangentsError, currentNode->mName.C_Str()); + } + + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); + AZStd::shared_ptr tangentStream = AZStd::make_shared(); - // AssImp only has one tangentStream per mesh. tangentStream->SetTangentSetIndex(0); tangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx); tangentStream->ReserveContainerSpace(vertexCount); - - for (int v = 0; v < mesh->mNumVertices; ++v) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - // Vector4's constructor that takes in a vector3 sets w to 1.0f automatically. - const Vector4 tangent(AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v])); - tangentStream->AppendTangent(tangent); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (!mesh->HasTangentsAndBitangents()) + { + // This node has mixed meshes with and without tangents. + // An error was already thrown above. Output stub tangents so + // the mesh can still be output in some form, even if the data isn't correct. + // The tangent count needs to match the vertex count on the associated mesh node. + tangentStream->AppendTangent(Vector4(0.f, 1.f, 0.f, 1.f)); + } + else + { + const Vector4 tangent( + AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v])); + tangentStream->AppendTangent(tangent); + } + } } - AZStd::string nodeName(AZStd::string::format("%s", m_defaultNodeName)); Containers::SceneGraph::NodeIndex newIndex = - context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str()); + context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName); Events::ProcessingResult tangentResults; - AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, nodeName.c_str()); + AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, m_defaultNodeName); tangentResults = Events::Process(dataPopulated); if (tangentResults != Events::ProcessingResult::Failure) { tangentResults = AddAttributeDataNodeWithContexts(dataPopulated); } - return tangentResults; } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp index 84c0e3e18c..5357c32fa9 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp @@ -50,7 +50,7 @@ namespace AZ Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context) { AZ_TraceContext("Importer", "transform"); - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName)) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp index f5f47b233d..e37a4f4285 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.cpp @@ -12,17 +12,19 @@ #include #include +#include +#include #include #include #include #include #include #include -#include -#include +#include #include #include -#include +#include +#include #include #include @@ -45,7 +47,7 @@ namespace AZ SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { - serializeContext->Class()->Version(3); // LYN-2506 + serializeContext->Class()->Version(4); // LYN-3250 } } @@ -56,28 +58,53 @@ namespace AZ { return Events::ProcessingResult::Ignored; } - aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); + const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context)); - if (!meshDataResult.IsSuccess()) + // AssImp separates meshes that have multiple materials. + // This code re-combines them to match previous FBX SDK behavior, + // so they can be separated by engine code instead. + bool foundTextureCoordinates = false; + AZStd::array meshesPerTextureCoordinateIndex = {}; + for (int localMeshIndex = 0; localMeshIndex < currentNode->mNumMeshes; ++localMeshIndex) { - return meshDataResult.GetError(); + aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[localMeshIndex]]; + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) + { + if (!mesh->mTextureCoords[texCoordIndex]) + { + continue; + } + ++meshesPerTextureCoordinateIndex[texCoordIndex]; + foundTextureCoordinates = true; + } } - const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue()); - size_t vertexCount = parentMeshData->GetVertexCount(); + if (!foundTextureCoordinates) + { + return Events::ProcessingResult::Ignored; + } - int sdkMeshIndex = parentMeshData->GetSdkMeshIndex(); - AZ_Assert(sdkMeshIndex >= 0, - "Tried to construct uv stream attribute for invalid or non-mesh parent data, mesh index is missing"); + const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene); - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) + { + int meshesWithIndex = meshesPerTextureCoordinateIndex[texCoordIndex]; + AZ_Error( + Utilities::ErrorWindow, + meshesWithIndex == 0 || meshesWithIndex == currentNode->mNumMeshes, + "Texture coordinate index %d for node %s is not on all meshes on this node. " + "Placeholder arbitrary texture values will be generated to allow the data to process, but the source art " + "needs to be fixed to correct this. All meshes on this node should have the same number of texture coordinate channels.", + texCoordIndex, + currentNode->mName.C_Str()); + } Events::ProcessingResultCombiner combinedUvMapResults; - for (int texCoordIndex = 0; texCoordIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++texCoordIndex) + for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex) { - if (!mesh->mTextureCoords[texCoordIndex]) + // No meshes have this texture coordinate index, skip it. + if (meshesPerTextureCoordinateIndex[texCoordIndex] == 0) { continue; } @@ -85,24 +112,55 @@ namespace AZ AZStd::shared_ptr uvMap = AZStd::make_shared(); uvMap->ReserveContainerSpace(vertexCount); - + bool customNameFound = false; AZStd::string name(AZStd::string::format("%s%d", m_defaultNodeName, texCoordIndex)); - if (mesh->mTextureCoordsNames[texCoordIndex].length) + for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex) { - name = mesh->mTextureCoordsNames[texCoordIndex].C_Str(); + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]]; + if(mesh->mTextureCoords[texCoordIndex]) + { + if (mesh->mTextureCoordsNames[texCoordIndex].length > 0) + { + if (!customNameFound) + { + name = mesh->mTextureCoordsNames[texCoordIndex].C_Str(); + customNameFound = true; + } + else + { + AZ_Warning(Utilities::WarningWindow, + strcmp(name.c_str(), mesh->mTextureCoordsNames[texCoordIndex].C_Str()) == 0, + "Node %s has conflicting mesh coordinate names at index %d, %s and %s. Using %s.", + currentNode->mName.C_Str(), + texCoordIndex, + name.c_str(), + mesh->mTextureCoordsNames[texCoordIndex].C_Str(), + name.c_str()); + } + } + } + + for (int v = 0; v < mesh->mNumVertices; ++v) + { + if (mesh->mTextureCoords[texCoordIndex]) + { + AZ::Vector2 vertexUV( + mesh->mTextureCoords[texCoordIndex][v].x, + // The engine's V coordinate is reverse of how it's stored in the FBX file. + 1.0f - mesh->mTextureCoords[texCoordIndex][v].y); + uvMap->AppendUV(vertexUV); + } + else + { + // An error was already emitted if the UV channels for all meshes on this node do not match. + // Append an arbitrary UV value so that the mesh can still be processed. + // It's better to let the engine load a partially valid mesh than to completely fail. + uvMap->AppendUV(AZ::Vector2::CreateZero()); + } + } } uvMap->SetCustomName(name.c_str()); - - for (int v = 0; v < mesh->mNumVertices; ++v) - { - AZ::Vector2 vertexUV( - mesh->mTextureCoords[texCoordIndex][v].x, - // The engine's V coordinate is reverse of how it's stored in the FBX file. - 1.0f - mesh->mTextureCoords[texCoordIndex][v].y); - uvMap->AppendUV(vertexUV); - } - Containers::SceneGraph::NodeIndex newIndex = context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, name.c_str()); @@ -116,6 +174,7 @@ namespace AZ } combinedUvMapResults += uvMapResults; + } return combinedUvMapResults.GetResult(); diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp index 59821336e0..c90fe7d1f3 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder { - bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, + bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, const AZStd::function()>& makeMeshFunc) { AZStd::unordered_map assImpMatIndexToLYIndex; @@ -34,17 +35,18 @@ namespace AZ::SceneAPI::FbxSceneBuilder { return false; } + auto newMesh = makeMeshFunc(); + newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters()); + newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters()); + + // AssImp separates meshes that have multiple materials. + // This code re-combines them to match previous FBX SDK behavior, + // so they can be separated by engine code instead. + int vertOffset = 0; for (int m = 0; m < currentNode->mNumMeshes; ++m) { - auto newMesh = makeMeshFunc(); - - newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters()); - newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters()); - - newMesh->SetSdkMeshIndex(m); - - aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]]; + const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]]; // Lumberyard materials are created in order based on mesh references in the scene if (assImpMatIndexToLYIndex.find(mesh->mMaterialIndex) == assImpMatIndexToLYIndex.end()) @@ -59,7 +61,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder sceneSystem.SwapVec3ForUpAxis(vertex); sceneSystem.ConvertUnit(vertex); newMesh->AddPosition(vertex); - newMesh->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx); + newMesh->SetVertexIndexToControlPointIndexMap(vertIdx + vertOffset, vertIdx + vertOffset); if (mesh->HasNormals()) { @@ -86,14 +88,15 @@ namespace AZ::SceneAPI::FbxSceneBuilder } for (int idx = 0; idx < face.mNumIndices; ++idx) { - meshFace.vertexIndex[idx] = face.mIndices[idx]; + meshFace.vertexIndex[idx] = face.mIndices[idx] + vertOffset; } newMesh->AddFace(meshFace, assImpMatIndexToLYIndex[mesh->mMaterialIndex]); } + vertOffset += mesh->mNumVertices; - meshes.push_back(newMesh); } + meshes.push_back(newMesh); return true; } @@ -127,4 +130,13 @@ namespace AZ::SceneAPI::FbxSceneBuilder azrtti_cast(parentData); return AZ::Success(parentMeshData); } + + uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene) + { + return AZStd::accumulate(node.mMeshes, node.mMeshes + node.mNumMeshes, uint64_t{ 0u }, + [&scene](auto runningTotal, unsigned int meshIndex) + { + return runningTotal + scene.mMeshes[meshIndex]->mNumVertices; + }); + } } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h index c0b5c044cb..3c7d3d2102 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h @@ -44,11 +44,16 @@ namespace AZ namespace FbxSceneBuilder { - bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, + bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector>& meshes, const AZStd::function()>& makeMeshFunc); typedef AZ::Outcome GetMeshDataFromParentResult; GetMeshDataFromParentResult GetMeshDataFromParent(AssImpSceneNodeAppendedContext& context); + + // If a node in the original scene file has a mesh with multiple materials on it, the associated AssImp + // node will have multiple meshes on it, broken apart per material. This returns the total number + // of vertices on all meshes on the given node. + uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene); } } } diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp index abc095d583..b67696e3cc 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/Utilities/RenamedNodesMap.cpp @@ -27,6 +27,7 @@ namespace AZ Containers::SceneGraph::NodeIndex parentNode, const char* defaultName) { AZ_TraceContext("Node name", name); + const AZStd::string originalNodeName(name); bool isNameUpdated = false; // Nodes can't have an empty name, except of the root, otherwise nodes can't be referenced. @@ -56,7 +57,7 @@ namespace AZ // can't reference the same parent in that case. This is to make sure the node can be quickly found as // the full path will be unique. To fix any issues, an index is appended. size_t index = 1; - size_t offset = name.length(); + const size_t offset = name.length(); while (graph.Find(parentNode, name).IsValid()) { // Remove the previously tried extension. @@ -71,7 +72,8 @@ namespace AZ if (isNameUpdated) { AZ_TraceContext("New node name", name); - AZ_TracePrintf(Utilities::WarningWindow, "The name of the node was invalid or conflicting and was updated."); + AZ_TracePrintf(Utilities::WarningWindow, "The name of the node '%s' was invalid or conflicting and was updated to '%s'.", + originalNodeName.c_str(), name.c_str()); } return isNameUpdated; diff --git a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp index eccbe729c6..2cda1e68ae 100644 --- a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp +++ b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp @@ -38,12 +38,14 @@ namespace AZ { AZ_TracePrintf(SceneAPI::Utilities::LogWindow, "AssImpSceneWrapper::LoadSceneFromFile %s", fileName); AZ_TraceContext("Filename", fileName); + // aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this, + // this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release. + // There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph. m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false); m_sceneFileName = fileName; m_assImpScene = m_importer.ReadFile(fileName, aiProcess_Triangulate //Triangulates all faces of all meshes - | aiProcess_JoinIdenticalVertices //Identifies and joins identical vertex data sets for the imported meshes | aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value //dropping the least important and re-normalizing | aiProcess_GenNormals); //Generate normals for meshes diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp index e99788d570..f3c01df2e3 100644 --- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp +++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.cpp @@ -19,6 +19,16 @@ namespace AZ::SceneAPI::Utilities m_output += AZStd::string::format("\t%s: %s\n", name, data); } + void DebugOutput::WriteArray(const char* name, const unsigned int* data, int size) + { + m_output += AZStd::string::format("\t%s: ", name); + for (int index = 0; index < size; ++index) + { + m_output += AZStd::string::format("%d, ", data[index]); + } + m_output += AZStd::string::format("\n"); + } + void DebugOutput::Write(const char* name, const AZStd::string& data) { Write(name, data.c_str()); diff --git a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h index a598c996c9..83dc9dd6ab 100644 --- a/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h +++ b/Code/Tools/SceneAPI/SceneCore/Utilities/DebugOutput.h @@ -29,6 +29,7 @@ namespace AZ::SceneAPI::Utilities void Write(const char* name, const AZStd::vector>& data); SCENE_CORE_API void Write(const char* name, const char* data); + SCENE_CORE_API void WriteArray(const char* name, const unsigned int* data, int size); SCENE_CORE_API void Write(const char* name, const AZStd::string& data); SCENE_CORE_API void Write(const char* name, double data); SCENE_CORE_API void Write(const char* name, uint64_t data); diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp index 902928d404..7d81166829 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp +++ b/Code/Tools/SceneAPI/SceneData/GraphData/BlendShapeData.cpp @@ -285,8 +285,26 @@ namespace AZ void BlendShapeData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const { output.Write("Positions", m_positions); + int index = 0; + for (const auto& position : m_positions) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), position); + ++index; + } + index = 0; output.Write("Normals", m_normals); + for (const auto& normal : m_normals) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), normal); + ++index; + } + index = 0; output.Write("Faces", m_faces); + for (const auto& face : m_faces) + { + output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3); + ++index; + } } } // GraphData } // SceneData diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp index 8bf49b2898..8f464240c9 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp +++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.cpp @@ -45,7 +45,6 @@ namespace AZ behaviorContext->Class() ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Module, "scene") - ->Method("GetSdkMeshIndex", &MeshData::GetSdkMeshIndex) ->Method("GetControlPointIndex", &MeshData::GetControlPointIndex) ->Method("GetUsedControlPointCount", &MeshData::GetUsedControlPointCount) ->Method("GetUsedPointIndexForControlPoint", &MeshData::GetUsedPointIndexForControlPoint) @@ -77,10 +76,6 @@ namespace AZ void MeshData::CloneAttributesFrom(const IGraphObject* sourceObject) { IMeshData::CloneAttributesFrom(sourceObject); - if (const auto* typedSource = azrtti_cast(sourceObject)) - { - SetSdkMeshIndex(typedSource->GetSdkMeshIndex()); - } } void MeshData::AddPosition(const AZ::Vector3& position) @@ -111,15 +106,6 @@ namespace AZ m_faceMaterialIds.push_back(faceMaterialId); } - void MeshData::SetSdkMeshIndex(int sdkMeshIndex) - { - m_sdkMeshIndex = sdkMeshIndex; - } - int MeshData::GetSdkMeshIndex() const - { - return m_sdkMeshIndex; - } - void MeshData::SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex) { m_vertexIndexToControlPointIndexMap[vertexIndex] = controlPointIndex; @@ -206,8 +192,26 @@ namespace AZ void MeshData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const { output.Write("Positions", m_positions); + int index = 0; + for (const auto& position : m_positions) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), position); + ++index; + } + index = 0; output.Write("Normals", m_normals); + for (const auto& normal : m_normals) + { + output.Write(AZStd::string::format("\t%d", index).c_str(), normal); + ++index; + } + index = 0; output.Write("FaceList", m_faceList); + for (const auto& face : m_faceList) + { + output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3); + ++index; + } output.Write("FaceMaterialIds", m_faceMaterialIds); } } diff --git a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h index c5197d6cb4..4321096857 100644 --- a/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h +++ b/Code/Tools/SceneAPI/SceneData/GraphData/MeshData.h @@ -49,9 +49,6 @@ namespace AZ SCENE_DATA_API void AddFace(const AZ::SceneAPI::DataTypes::IMeshData::Face& face, unsigned int faceMaterialId = AZ::SceneAPI::DataTypes::IMeshData::s_invalidMaterialId); - SCENE_DATA_API void SetSdkMeshIndex(int sdkMeshIndex); - SCENE_DATA_API int GetSdkMeshIndex() const; - SCENE_DATA_API void SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex); SCENE_DATA_API size_t GetUsedControlPointCount() const override; SCENE_DATA_API int GetControlPointIndex(int vertexIndex) const override; @@ -80,8 +77,6 @@ namespace AZ AZStd::unordered_map m_vertexIndexToControlPointIndexMap; AZStd::unordered_map m_controlPointToUsedVertexIndexMap; - - int m_sdkMeshIndex = -1; }; } } diff --git a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp index 79c6cfea7e..84018bbc9a 100644 --- a/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp +++ b/Code/Tools/SceneAPI/SceneData/Tests/GraphData/GraphDataBehaviorTests.cpp @@ -57,7 +57,6 @@ namespace AZ meshData->AddNormal(Vector3{0.1f, 0.2f, 0.3f}); meshData->AddNormal(Vector3{0.4f, 0.5f, 0.6f}); meshData->SetOriginalUnitSizeInMeters(10.0f); - meshData->SetSdkMeshIndex(1337); meshData->SetUnitSizeInMeters(0.5f); meshData->SetVertexIndexToControlPointIndexMap(0, 10); meshData->SetVertexIndexToControlPointIndexMap(1, 11); @@ -252,7 +251,6 @@ namespace AZ ExpectExecute("TestExpectFloatEquals(meshData:GetNormal(1).z, 0.6)"); ExpectExecute("TestExpectFloatEquals(meshData:GetOriginalUnitSizeInMeters(), 10.0)"); ExpectExecute("TestExpectFloatEquals(meshData:GetUnitSizeInMeters(), 0.5)"); - ExpectExecute("TestExpectIntegerEquals(meshData:GetSdkMeshIndex(), 1337)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetUsedControlPointCount(), 4)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(0), 10)"); ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(1), 11)"); diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl index 84095ac163..456d7cbabe 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.azsl @@ -101,7 +101,7 @@ struct VSOutput float2 m_uv[UvSetCount] : UV1; float2 m_detailUv : UV3; - float4 m_blendMask : UV8; + float4 m_wrinkleBlendFactors : UV8; }; #include @@ -132,11 +132,11 @@ VSOutput SkinVS(VSInput IN) if(o_blendMask_isBound) { - OUT.m_blendMask = IN.m_optional_blendMask; + OUT.m_wrinkleBlendFactors = IN.m_optional_blendMask; } else { - OUT.m_blendMask = float4(0,1,0,0); + OUT.m_wrinkleBlendFactors = float4(0,0,0,0); } VertexHelper(IN, OUT, worldPosition, false); @@ -214,7 +214,22 @@ PbrLightingOutput SkinPS_Common(VSOutput IN) float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex]; float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor; - + + // ------- Wrinkle Map Setup ------- + + // Combine the optional per-morph target wrinkle masks + float4 wrinkleBlendFactors = float4(0.0, 0.0, 0.0, 0.0); + for(uint wrinkleMaskIndex = 0; wrinkleMaskIndex < ObjectSrg::m_wrinkle_mask_count; ++wrinkleMaskIndex) + { + wrinkleBlendFactors += ObjectSrg::m_wrinkle_masks[wrinkleMaskIndex].Sample(MaterialSrg::m_sampler, normalUv) * ObjectSrg::GetWrinkleMaskWeight(wrinkleMaskIndex); + } + + // If texture based morph target driven masks are being used, use those values instead of the per-vertex colors + if(ObjectSrg::m_wrinkle_mask_count) + { + IN.m_wrinkleBlendFactors = saturate(wrinkleBlendFactors); + } + // Since the wrinkle normal maps should all be in the same tangent space as the main normal map, we should be able to blend the raw normal map // texture values before doing all the tangent space transforms, so we only have to do the transforms once, for better performance. @@ -223,12 +238,12 @@ PbrLightingOutput SkinPS_Common(VSOutput IN) { normalMapSample = SampleNormalXY(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY); } - if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_normal_enabled) + if(o_wrinkleLayers_enabled && o_wrinkleLayers_normal_enabled) { - normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.r); - normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.g); - normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.b); - normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_blendMask.a); + normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture1, normalMapSample, MaterialSrg::m_wrinkle_normal_texture1, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.r); + normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture2, normalMapSample, MaterialSrg::m_wrinkle_normal_texture2, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.g); + normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture3, normalMapSample, MaterialSrg::m_wrinkle_normal_texture3, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.b); + normalMapSample = ApplyNormalWrinkleMap(o_wrinkleLayers_normal_useTexture4, normalMapSample, MaterialSrg::m_wrinkle_normal_texture4, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, IN.m_wrinkleBlendFactors.a); } if(o_detail_normal_useTexture) @@ -255,7 +270,7 @@ PbrLightingOutput SkinPS_Common(VSOutput IN) float3 baseColor = GetBaseColorInput(MaterialSrg::m_baseColorMap, MaterialSrg::m_sampler, baseColorUv, MaterialSrg::m_baseColor, o_baseColor_useTexture); bool useSampledBaseColor = o_baseColor_useTexture; - if(o_wrinkleLayers_enabled && o_blendMask_isBound && o_wrinkleLayers_baseColor_enabled) + if(o_wrinkleLayers_enabled && o_wrinkleLayers_baseColor_enabled) { // If any of the wrinkle maps are applied, we will use the Base Color blend settings to apply the MaterialSrg::m_baseColor tint to the wrinkle maps, // even if the main base color map is not used. @@ -272,10 +287,10 @@ PbrLightingOutput SkinPS_Common(VSOutput IN) baseColor = float3(1,1,1); } - baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.r); - baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.g); - baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.b); - baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_blendMask.a); + baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture1, baseColor, MaterialSrg::m_wrinkle_baseColor_texture1, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.r); + baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture2, baseColor, MaterialSrg::m_wrinkle_baseColor_texture2, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.g); + baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture3, baseColor, MaterialSrg::m_wrinkle_baseColor_texture3, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.b); + baseColor = ApplyBaseColorWrinkleMap(o_wrinkleLayers_baseColor_useTexture4, baseColor, MaterialSrg::m_wrinkle_baseColor_texture4, MaterialSrg::m_sampler, baseColorUv, IN.m_wrinkleBlendFactors.a); } @@ -283,13 +298,13 @@ PbrLightingOutput SkinPS_Common(VSOutput IN) baseColor = ApplyTextureOverlay(o_detail_baseColor_useTexture, baseColor, MaterialSrg::m_detail_baseColor_texture, MaterialSrg::m_sampler, IN.m_detailUv, detailLayerBaseColorFactor); - if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues && o_blendMask_isBound) + if(o_wrinkleLayers_enabled && o_wrinkleLayers_showBlendMaskValues) { // Overlay debug colors to highlight the different blend weights coming from the vertex color stream. - if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_blendMask.r); } - if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_blendMask.g); } - if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_blendMask.b); } - if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_blendMask.a); } + if(o_wrinkleLayers_count > 0) { baseColor = lerp(baseColor, float3(1,0,0), IN.m_wrinkleBlendFactors.r); } + if(o_wrinkleLayers_count > 1) { baseColor = lerp(baseColor, float3(0,1,0), IN.m_wrinkleBlendFactors.g); } + if(o_wrinkleLayers_count > 2) { baseColor = lerp(baseColor, float3(0,0,1), IN.m_wrinkleBlendFactors.b); } + if(o_wrinkleLayers_count > 3) { baseColor = lerp(baseColor, float3(1,1,1), IN.m_wrinkleBlendFactors.a); } } // ------- Specular ------- diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index b8951d69c7..101a03b907 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -987,15 +987,6 @@ { "file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader", "tag": "SkinnedMeshMotionVector" - }, - // Used by the light culling system to produce accurate depth bounds for this object when it uses blended transparency - { - "file": "Shaders/Depth/DepthPassTransparentMin.shader", - "tag": "DepthPassTransparentMin" - }, - { - "file": "Shaders/Depth/DepthPassTransparentMax.shader", - "tag": "DepthPassTransparentMax" } ], "functors": [ diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index a74ceb1783..2b0d09bc5c 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -1199,6 +1199,14 @@ "file": "./StandardPBR_ForwardPass_EDS.shader", "tag": "ForwardPass_EDS" }, + { + "file": "./StandardPBR_LowEndForward.shader", + "tag": "LowEndForward" + }, + { + "file": "./StandardPBR_LowEndForward_EDS.shader", + "tag": "LowEndForward_EDS" + }, { "file": "Shaders/Shadow/Shadowmap.shader", "tag": "Shadowmap" @@ -1289,10 +1297,6 @@ "textureProperty": "baseColor.textureMap", "useTextureProperty": "baseColor.useTexture", "dependentProperties": ["baseColor.textureMapUv", "baseColor.textureBlendMode"], - "shaderTags": [ - "ForwardPass", - "ForwardPass_EDS" - ], "shaderOption": "o_baseColor_useTexture" } }, @@ -1302,10 +1306,6 @@ "textureProperty": "metallic.textureMap", "useTextureProperty": "metallic.useTexture", "dependentProperties": ["metallic.textureMapUv"], - "shaderTags": [ - "ForwardPass", - "ForwardPass_EDS" - ], "shaderOption": "o_metallic_useTexture" } }, @@ -1315,10 +1315,6 @@ "textureProperty": "specularF0.textureMap", "useTextureProperty": "specularF0.useTexture", "dependentProperties": ["specularF0.textureMapUv"], - "shaderTags": [ - "ForwardPass", - "ForwardPass_EDS" - ], "shaderOption": "o_specularF0_useTexture" } }, @@ -1328,10 +1324,6 @@ "textureProperty": "normal.textureMap", "useTextureProperty": "normal.useTexture", "dependentProperties": ["normal.textureMapUv", "normal.factor", "normal.flipX", "normal.flipY"], - "shaderTags": [ - "ForwardPass", - "ForwardPass_EDS" - ], "shaderOption": "o_normal_useTexture" } }, diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl index f362349a7b..bf1ec96b79 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl @@ -10,6 +10,8 @@ * */ +#include "Atom/Features/ShaderQualityOptions.azsli" + #include "StandardPBR_Common.azsli" // SRGs @@ -317,13 +319,18 @@ ForwardPassOutputWithDepth StandardPbr_ForwardPassPS(VSOutput IN, bool isFrontFa PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth); +#ifdef UNIFIED_FORWARD_OUTPUT + OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb; + OUT.m_color.a = lightingOutput.m_diffuseColor.a; + OUT.m_depth = depth; +#else OUT.m_diffuseColor = lightingOutput.m_diffuseColor; OUT.m_specularColor = lightingOutput.m_specularColor; OUT.m_specularF0 = lightingOutput.m_specularF0; OUT.m_albedo = lightingOutput.m_albedo; OUT.m_normal = lightingOutput.m_normal; OUT.m_depth = depth; - +#endif return OUT; } @@ -335,12 +342,16 @@ ForwardPassOutput StandardPbr_ForwardPassPS_EDS(VSOutput IN, bool isFrontFace : PbrLightingOutput lightingOutput = ForwardPassPS_Common(IN, isFrontFace, depth); +#ifdef UNIFIED_FORWARD_OUTPUT + OUT.m_color.rgb = lightingOutput.m_diffuseColor.rgb + lightingOutput.m_specularColor.rgb; + OUT.m_color.a = lightingOutput.m_diffuseColor.a; +#else OUT.m_diffuseColor = lightingOutput.m_diffuseColor; OUT.m_specularColor = lightingOutput.m_specularColor; OUT.m_specularF0 = lightingOutput.m_specularF0; OUT.m_albedo = lightingOutput.m_albedo; OUT.m_normal = lightingOutput.m_normal; - +#endif return OUT; } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl new file mode 100644 index 0000000000..c87faffcbe --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.azsl @@ -0,0 +1,17 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +// NOTE: This file is a temporary workaround until .shader files can #define macros for their .azsl files + +#define QUALITY_LOW_END 1 + +#include "StandardPBR_ForwardPass.azsl" diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader new file mode 100644 index 0000000000..44139608ca --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward.shader @@ -0,0 +1,59 @@ +{ + // Note: "LowEnd" shaders are for supporting the low end pipeline + // These shaders can be safely added to materials without incurring additional runtime draw + // items as draw items for shaders are only created if the scene has a pass with a matching + // DrawListTag. If your pipeline doesn't have a "lowEndForward" DrawListTag, no draw items + // for this shader will be created. + + "Source" : "./StandardPBR_LowEndForward.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "CompareFunc" : "GreaterEqual" + }, + "Stencil" : + { + "Enable" : true, + "ReadMask" : "0x00", + "WriteMask" : "0xFF", + "FrontFace" : + { + "Func" : "Always", + "DepthFailOp" : "Keep", + "FailOp" : "Keep", + "PassOp" : "Replace" + }, + "BackFace" : + { + "Func" : "Always", + "DepthFailOp" : "Keep", + "FailOp" : "Keep", + "PassOp" : "Replace" + } + } + }, + + "CompilerHints" : { + "DisableOptimizations" : false + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "StandardPbr_ForwardPassVS", + "type": "Vertex" + }, + { + "name": "StandardPbr_ForwardPassPS", + "type": "Fragment" + } + ] + }, + + "DrawList" : "lowEndForward" +} diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader new file mode 100644 index 0000000000..9faa1d3698 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_LowEndForward_EDS.shader @@ -0,0 +1,59 @@ +{ + // Note: "LowEnd" shaders are for supporting the low end pipeline + // These shaders can be safely added to materials without incurring additional runtime draw + // items as draw items for shaders are only created if the scene has a pass with a matching + // DrawListTag. If your pipeline doesn't have a "lowEndForward" DrawListTag, no draw items + // for this shader will be created. + + "Source" : "./StandardPBR_LowEndForward.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "CompareFunc" : "GreaterEqual" + }, + "Stencil" : + { + "Enable" : true, + "ReadMask" : "0x00", + "WriteMask" : "0xFF", + "FrontFace" : + { + "Func" : "Always", + "DepthFailOp" : "Keep", + "FailOp" : "Keep", + "PassOp" : "Replace" + }, + "BackFace" : + { + "Func" : "Always", + "DepthFailOp" : "Keep", + "FailOp" : "Keep", + "PassOp" : "Replace" + } + } + }, + + "CompilerHints" : { + "DisableOptimizations" : false + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "StandardPbr_ForwardPassVS", + "type": "Vertex" + }, + { + "name": "StandardPbr_ForwardPassPS_EDS", + "type": "Fragment" + } + ] + }, + + "DrawList" : "lowEndForward" +} diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua index 7c3d989c35..2733713122 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ShaderEnable.lua @@ -29,26 +29,33 @@ function Process(context) local depthPass = context:GetShaderByTag("DepthPass") local shadowMap = context:GetShaderByTag("Shadowmap") local forwardPassEDS = context:GetShaderByTag("ForwardPass_EDS") + local lowEndForwardEDS = context:GetShaderByTag("LowEndForward_EDS") + local depthPassWithPS = context:GetShaderByTag("DepthPass_WithPS") local shadowMapWitPS = context:GetShaderByTag("Shadowmap_WithPS") local forwardPass = context:GetShaderByTag("ForwardPass") + local lowEndForward = context:GetShaderByTag("LowEndForward") if parallaxEnabled and parallaxPdoEnabled then depthPass:SetEnabled(false) shadowMap:SetEnabled(false) forwardPassEDS:SetEnabled(false) + lowEndForwardEDS:SetEnabled(false) depthPassWithPS:SetEnabled(true) shadowMapWitPS:SetEnabled(true) forwardPass:SetEnabled(true) + lowEndForward:SetEnabled(true) else depthPass:SetEnabled(opacityMode == OpacityMode_Opaque) shadowMap:SetEnabled(opacityMode == OpacityMode_Opaque) forwardPassEDS:SetEnabled((opacityMode == OpacityMode_Opaque) or (opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent)) + lowEndForwardEDS:SetEnabled((opacityMode == OpacityMode_Opaque) or (opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent)) depthPassWithPS:SetEnabled(opacityMode == OpacityMode_Cutout) shadowMapWitPS:SetEnabled(opacityMode == OpacityMode_Cutout) forwardPass:SetEnabled(opacityMode == OpacityMode_Cutout) + lowEndForward:SetEnabled(opacityMode == OpacityMode_Cutout) end context:GetShaderByTag("DepthPassTransparentMin"):SetEnabled((opacityMode == OpacityMode_Blended) or (opacityMode == OpacityMode_TintedTransparent)) diff --git a/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass b/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass index 31a8ed1879..b66e3bb4e1 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/Forward.pass @@ -148,22 +148,6 @@ }, "LoadAction": "Clear" } - }, - { - "Name": "ScatterDistanceOutput", - "SlotType": "Output", - "ScopeAttachmentUsage": "RenderTarget", - "LoadStoreAction": { - "ClearValue": { - "Value": [ - 0.0, - 0.0, - 0.0, - 0.0 - ] - }, - "LoadAction": "Clear" - } } ], "ImageAttachments": [ @@ -238,19 +222,6 @@ "AssetRef": { "FilePath": "Textures/BRDFTexture.attimage" } - }, - { - "Name": "ScatterDistanceImage", - "SizeSource": { - "Source": { - "Pass": "Parent", - "Attachment": "SwapChainOutput" - } - }, - "ImageDescriptor": { - "Format": "R11G11B10_FLOAT", - "SharedQueueMask": "Graphics" - } } ], "Connections": [ @@ -295,13 +266,6 @@ "Pass": "This", "Attachment": "BRDFTexture" } - }, - { - "LocalSlot": "ScatterDistanceOutput", - "AttachmentRef": { - "Pass": "This", - "Attachment": "ScatterDistanceImage" - } } ] } diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass new file mode 100644 index 0000000000..3e804d23e2 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass @@ -0,0 +1,146 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "LightAdaptationParentTemplate", + "PassClass": "ParentPass", + "Slots": [ + // Inputs... + { + "Name": "LightingInput", + "SlotType": "Input" + }, + // SwapChain here is only used to reference the frame height and format + { + "Name": "SwapChainOutput", + "SlotType": "InputOutput" + }, + // Outputs... + { + "Name": "Output", + "SlotType": "Output" + }, + // Debug Outputs... + { + "Name": "LuminanceMipChainOutput", + "SlotType": "Output" + } + ], + "Connections": [ + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "DisplayMapperPass", + "Attachment": "Output" + } + }, + { + "LocalSlot": "LuminanceMipChainOutput", + "AttachmentRef": { + "Pass": "DownsampleLuminanceMipChain", + "Attachment": "MipChainInputOutput" + } + } + ], + "PassRequests": [ + { + "Name": "DownsampleLuminanceMinAvgMax", + "TemplateName": "DownsampleLuminanceMinAvgMaxCS", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightingInput" + } + } + ] + }, + { + "Name": "DownsampleLuminanceMipChain", + "TemplateName": "DownsampleMipChainTemplate", + "Connections": [ + { + "LocalSlot": "MipChainInputOutput", + "AttachmentRef": { + "Pass": "DownsampleLuminanceMinAvgMax", + "Attachment": "Output" + } + } + ], + "PassData": { + "$type": "DownsampleMipChainPassData", + "ShaderAsset": { + "FilePath": "Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader" + } + } + }, + { + "Name": "EyeAdaptationPass", + "TemplateName": "EyeAdaptationTemplate", + "Enabled": false, + "Connections": [ + { + "LocalSlot": "SceneLuminanceInput", + "AttachmentRef": { + "Pass": "DownsampleLuminanceMipChain", + "Attachment": "MipChainInputOutput" + } + } + ] + }, + { + "Name": "LookModificationTransformPass", + "TemplateName": "LookModificationTransformTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightingInput" + } + }, + { + "LocalSlot": "EyeAdaptationDataInput", + "AttachmentRef": { + "Pass": "EyeAdaptationPass", + "Attachment": "EyeAdaptationDataInputOutput" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + }, + { + "Name": "DisplayMapperPass", + "TemplateName": "DisplayMapperTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "LookModificationTransformPass", + "Attachment": "Output" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + } + ] + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass b/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass new file mode 100644 index 0000000000..4b865fcb6d --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/LowEndForward.pass @@ -0,0 +1,133 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "LowEndForwardPassTemplate", + "PassClass": "RasterPass", + "Slots": [ + // Inputs... + { + "Name": "BRDFTextureInput", + "ShaderInputName": "m_brdfMap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "DirectionalLightShadowmap", + "ShaderInputName": "m_directionalLightShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ExponentialShadowmapDirectional", + "ShaderInputName": "m_directionalLightExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedShadowmap", + "ShaderInputName": "m_projectedShadowmaps", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ExponentialShadowmapProjected", + "ShaderInputName": "m_projectedExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "TileLightData", + "SlotType": "Input", + "ShaderInputName": "m_tileLightData", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input", + "ShaderInputName": "m_lightListRemapped", + "ScopeAttachmentUsage": "Shader" + }, + // Input/Outputs... + { + "Name": "DepthStencilInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" + }, + // Outputs... + { + "Name": "LightingOutput", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "ClearValue": { + "Value": [ + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "LoadAction": "Clear" + } + } + ], + "ImageAttachments": [ + { + "Name": "LightingAttachment", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + }, + "MultisampleSource": { + "Pass": "This", + "Attachment": "DepthStencilInputOutput" + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT", + "SharedQueueMask": "Graphics" + } + }, + { + "Name": "BRDFTexture", + "Lifetime": "Imported", + "AssetRef": { + "FilePath": "Textures/BRDFTexture.attimage" + } + } + ], + "Connections": [ + { + "LocalSlot": "LightingOutput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "LightingAttachment" + } + }, + { + "LocalSlot": "BRDFTextureInput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "BRDFTexture" + } + } + ] + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass new file mode 100644 index 0000000000..b19569fb9d --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass @@ -0,0 +1,344 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "LowEndPipelineTemplate", + "PassClass": "ParentPass", + "Slots": [ + { + "Name": "SwapChainOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + } + ], + "PassRequests": [ + { + "Name": "MorphTargetPass", + "TemplateName": "MorphTargetPassTemplate" + }, + { + "Name": "SkinningPass", + "TemplateName": "SkinningPassTemplate", + "Connections": [ + { + "LocalSlot": "SkinnedMeshOutputStream", + "AttachmentRef": { + "Pass": "MorphTargetPass", + "Attachment": "MorphTargetDeltaOutput" + } + } + ] + }, + { + "Name": "DepthPrePass", + "TemplateName": "DepthMSAAParentTemplate", + "Connections": [ + { + "LocalSlot": "SkinnedMeshes", + "AttachmentRef": { + "Pass": "SkinningPass", + "Attachment": "SkinnedMeshOutputStream" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + }, + { + "Name": "LightCullingPass", + "TemplateName": "LightCullingParentTemplate", + "Connections": [ + { + "LocalSlot": "SkinnedMeshes", + "AttachmentRef": { + "Pass": "SkinningPass", + "Attachment": "SkinnedMeshOutputStream" + } + }, + { + "LocalSlot": "DepthMSAA", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "DepthMSAA" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + }, + { + "Name": "ShadowPass", + "TemplateName": "ShadowParentTemplate", + "Connections": [ + { + "LocalSlot": "SkinnedMeshes", + "AttachmentRef": { + "Pass": "SkinningPass", + "Attachment": "SkinnedMeshOutputStream" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + }, + { + "Name": "ForwardPass", + "TemplateName": "LowEndForwardPassTemplate", + "Connections": [ + // Inputs... + { + "LocalSlot": "DirectionalLightShadowmap", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "DirectionalShadowmap" + } + }, + { + "LocalSlot": "ExponentialShadowmapDirectional", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "DirectionalESM" + } + }, + { + "LocalSlot": "ProjectedShadowmap", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "ProjectedShadowmap" + } + }, + { + "LocalSlot": "ExponentialShadowmapProjected", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "ProjectedESM" + } + }, + { + "LocalSlot": "TileLightData", + "AttachmentRef": { + "Pass": "LightCullingPass", + "Attachment": "TileLightData" + } + }, + { + "LocalSlot": "LightListRemapped", + "AttachmentRef": { + "Pass": "LightCullingPass", + "Attachment": "LightListRemapped" + } + }, + // Input/Outputs... + { + "LocalSlot": "DepthStencilInputOutput", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "DepthMSAA" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "lowEndForward", + "PipelineViewTag": "MainCamera", + "PassSrgAsset": { + "FilePath": "shaderlib/atom/features/pbr/forwardpasssrg.azsli:PassSrg" + } + } + }, + { + "Name": "SkyBoxPass", + "TemplateName": "SkyBoxTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SpecularInputOutput", + "AttachmentRef": { + "Pass": "ForwardPass", + "Attachment": "LightingOutput" + } + }, + { + "LocalSlot": "SkyBoxDepth", + "AttachmentRef": { + "Pass": "ForwardPass", + "Attachment": "DepthStencilInputOutput" + } + } + ] + }, + { + "Name": "MSAAResolvePass", + "TemplateName": "MSAAResolveColorTemplate", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "SkyBoxPass", + "Attachment": "SpecularInputOutput" + } + } + ] + }, + { + "Name": "TransparentPass", + "TemplateName": "TransparentParentTemplate", + "Connections": [ + { + "LocalSlot": "DirectionalShadowmap", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "DirectionalShadowmap" + } + }, + { + "LocalSlot": "DirectionalESM", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "DirectionalESM" + } + }, + { + "LocalSlot": "ProjectedShadowmap", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "ProjectedShadowmap" + } + }, + { + "LocalSlot": "ProjectedESM", + "AttachmentRef": { + "Pass": "ShadowPass", + "Attachment": "ProjectedESM" + } + }, + { + "LocalSlot": "TileLightData", + "AttachmentRef": { + "Pass": "LightCullingPass", + "Attachment": "TileLightData" + } + }, + { + "LocalSlot": "LightListRemapped", + "AttachmentRef": { + "Pass": "LightCullingPass", + "Attachment": "LightListRemapped" + } + }, + { + "LocalSlot": "DepthStencil", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "InputOutput", + "AttachmentRef": { + "Pass": "MSAAResolvePass", + "Attachment": "Output" + } + } + ] + }, + { + "Name": "LightAdaptation", + "TemplateName": "LightAdaptationParentTemplate", + "Connections": [ + { + "LocalSlot": "LightingInput", + "AttachmentRef": { + "Pass": "TransparentPass", + "Attachment": "InputOutput" + } + }, + { + "LocalSlot": "SwapChainOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + }, + { + "Name": "AuxGeomPass", + "TemplateName": "AuxGeomPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "ColorInputOutput", + "AttachmentRef": { + "Pass": "LightAdaptation", + "Attachment": "Output" + } + }, + { + "LocalSlot": "DepthInputOutput", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "Depth" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "auxgeom", + "PipelineViewTag": "MainCamera" + } + }, + { + "Name": "UIPass", + "TemplateName": "UIParentTemplate", + "Connections": [ + { + "LocalSlot": "InputOutput", + "AttachmentRef": { + "Pass": "AuxGeomPass", + "Attachment": "ColorInputOutput" + } + } + ] + }, + { + "Name": "CopyToSwapChain", + "TemplateName": "FullscreenCopyTemplate", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "UIPass", + "Attachment": "InputOutput" + } + }, + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "SwapChainOutput" + } + } + ] + } + ] + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass index dda120e164..a691fe2534 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/OpaqueParent.pass @@ -305,7 +305,7 @@ }, { "Name": "SkyBoxPass", - "TemplateName": "SkyBoxTemplate", + "TemplateName": "SkyBoxTwoOutputsTemplate", "Enabled": true, "Connections": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset index b83ab65ff2..c56e8932b1 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset +++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset @@ -92,6 +92,10 @@ "Name": "SkyBoxTemplate", "Path": "Passes/SkyBox.pass" }, + { + "Name": "SkyBoxTwoOutputsTemplate", + "Path": "Passes/SkyBox_TwoOutputs.pass" + }, { "Name": "UIPassTemplate", "Path": "Passes/UI.pass" @@ -483,6 +487,18 @@ { "Name": "UIParentTemplate", "Path": "Passes/UIParent.pass" + }, + { + "Name": "LightAdaptationParentTemplate", + "Path": "Passes/LightAdaptationParent.pass" + }, + { + "Name": "LowEndForwardPassTemplate", + "Path": "Passes/LowEndForward.pass" + }, + { + "Name": "LowEndPipelineTemplate", + "Path": "Passes/LowEndPipeline.pass" } ] } diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass index 37b1ee5c5a..36f7f1e985 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/PostProcessParent.pass @@ -40,7 +40,7 @@ { "LocalSlot": "Output", "AttachmentRef": { - "Pass": "DisplayMapperPass", + "Pass": "LightAdaptation", "Attachment": "Output" } }, @@ -54,8 +54,8 @@ { "LocalSlot": "LuminanceMipChainOutput", "AttachmentRef": { - "Pass": "DownsampleLuminanceMipChain", - "Attachment": "MipChainInputOutput" + "Pass": "LightAdaptation", + "Attachment": "LuminanceMipChainOutput" } } ], @@ -115,94 +115,16 @@ } ] }, - // Everything before this point deals in raw lighting values - // --------------------------------------------------------- - // Everything after starts to map to values we see on screen { - "Name": "DownsampleLuminanceMinAvgMax", - "TemplateName": "DownsampleLuminanceMinAvgMaxCS", + "Name": "LightAdaptation", + "TemplateName": "LightAdaptationParentTemplate", "Connections": [ { - "LocalSlot": "Input", + "LocalSlot": "LightingInput", "AttachmentRef": { "Pass": "BloomPass", "Attachment": "InputOutput" } - } - ] - }, - { - "Name": "DownsampleLuminanceMipChain", - "TemplateName": "DownsampleMipChainTemplate", - "Connections": [ - { - "LocalSlot": "MipChainInputOutput", - "AttachmentRef": { - "Pass": "DownsampleLuminanceMinAvgMax", - "Attachment": "Output" - } - } - ], - "PassData": { - "$type": "DownsampleMipChainPassData", - "ShaderAsset": { - "FilePath": "Shaders/PostProcessing/DownsampleMinAvgMaxCS.shader" - } - } - }, - { - "Name": "EyeAdaptationPass", - "TemplateName": "EyeAdaptationTemplate", - "Enabled": false, - "Connections": [ - { - "LocalSlot": "SceneLuminanceInput", - "AttachmentRef": { - "Pass": "DownsampleLuminanceMipChain", - "Attachment": "MipChainInputOutput" - } - } - ] - }, - { - "Name": "LookModificationTransformPass", - "TemplateName": "LookModificationTransformTemplate", - "Enabled": true, - "Connections": [ - { - "LocalSlot": "Input", - "AttachmentRef": { - "Pass": "BloomPass", - "Attachment": "InputOutput" - } - }, - { - "LocalSlot": "EyeAdaptationDataInput", - "AttachmentRef": { - "Pass": "EyeAdaptationPass", - "Attachment": "EyeAdaptationDataInputOutput" - } - }, - { - "LocalSlot": "SwapChainOutput", - "AttachmentRef": { - "Pass": "Parent", - "Attachment": "SwapChainOutput" - } - } - ] - }, - { - "Name": "DisplayMapperPass", - "TemplateName": "DisplayMapperTemplate", - "Enabled": true, - "Connections": [ - { - "LocalSlot": "Input", - "AttachmentRef": { - "Pass": "LookModificationTransformPass", - "Attachment": "Output" - } }, { "LocalSlot": "SwapChainOutput", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass index 57f442e5de..fb16271ba7 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox.pass @@ -12,11 +12,6 @@ "SlotType": "InputOutput", "ScopeAttachmentUsage": "RenderTarget" }, - { - "Name": "ReflectionInputOutput", - "SlotType": "InputOutput", - "ScopeAttachmentUsage": "RenderTarget" - }, { "Name": "SkyBoxDepth", "SlotType": "InputOutput", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass new file mode 100644 index 0000000000..0ed7b39288 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Passes/SkyBox_TwoOutputs.pass @@ -0,0 +1,43 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "SkyBoxTwoOutputsTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + "Name": "SpecularInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + }, + { + "Name": "ReflectionInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + }, + { + "Name": "SkyBoxDepth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + "FilePath": "shaders/skybox/skybox_twooutputs.shader" + }, + "PipelineViewTag": "MainCamera", + "ShaderDataMappings": { + "FloatMappings": [ + { + "Name": "m_sunIntensityMultiplier", + "Value": 1.0 + } + ] + } + } + } + } +} diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli index 50896cdf25..abc4ec7fc4 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli @@ -31,6 +31,16 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject return SceneSrg::GetObjectToWorldInverseTransposeMatrix(m_objectId); } + //[GFX TODO][ATOM-15280] Move wrinkle mask data from the default object srg into something specific to the Skin shader + uint m_wrinkle_mask_count; + float4 m_wrinkle_mask_weights[4]; + Texture2D m_wrinkle_masks[16]; + + float GetWrinkleMaskWeight(uint index) + { + return m_wrinkle_mask_weights[index / 4][index % 4]; + } + //! Reflection Probe (smallest probe volume that overlaps the object position) struct ReflectionProbeData { diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli index acc215f1c9..5821deb3b1 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/ForwardPassOutput.azsli @@ -10,6 +10,21 @@ * */ +#ifdef UNIFIED_FORWARD_OUTPUT + +struct ForwardPassOutput +{ + float4 m_color : SV_Target0; +}; + +struct ForwardPassOutputWithDepth +{ + float4 m_color : SV_Target0; + float m_depth : SV_Depth; +}; + +#else + struct ForwardPassOutput { float4 m_diffuseColor : SV_Target0; //!< RGB = Diffuse Lighting, A = Blend Alpha (for blended surfaces) OR A = special encoding of surfaceScatteringFactor, m_subsurfaceScatteringQuality, o_enableSubsurfaceScattering @@ -30,3 +45,5 @@ struct ForwardPassOutputWithDepth float4 m_normal : SV_Target4; float m_depth : SV_Depth; }; + +#endif diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli index 7400005508..3be9d5756a 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli @@ -12,38 +12,39 @@ #pragma once +// --- Static Options Available --- +// FORCE_IBL_IN_FORWARD_PASS - forces IBL lighting to be run in the forward pass, used in pipelines that don't have a reflection pass + #include #include #include #include -void ApplyIblDiffuse( +float3 GetIblDiffuse( float3 normal, float3 albedo, - float3 diffuseResponse, - out float3 outDiffuse) + float3 diffuseResponse) { float3 irradianceDir = MultiplyVectorQuaternion(normal, SceneSrg::m_iblOrientation); float3 diffuseSample = SceneSrg::m_diffuseEnvMap.Sample(SceneSrg::m_samplerEnv, GetCubemapCoords(irradianceDir)).rgb; - outDiffuse = diffuseResponse * albedo * diffuseSample; + return diffuseResponse * albedo * diffuseSample; } -void ApplyIblSpecular( +float3 GetIblSpecular( float3 position, float3 normal, float3 specularF0, float roughnessLinear, float3 dirToCamera, - float2 brdf, - out float3 outSpecular) + float2 brdf) { float3 reflectDir = reflect(-dirToCamera, normal); reflectDir = MultiplyVectorQuaternion(reflectDir, SceneSrg::m_iblOrientation); // global - outSpecular = SceneSrg::m_specularEnvMap.SampleLevel(SceneSrg::m_samplerEnv, GetCubemapCoords(reflectDir), GetRoughnessMip(roughnessLinear)).rgb; + float3 outSpecular = SceneSrg::m_specularEnvMap.SampleLevel(SceneSrg::m_samplerEnv, GetCubemapCoords(reflectDir), GetRoughnessMip(roughnessLinear)).rgb; outSpecular *= (specularF0 * brdf.x + brdf.y); // reflection probe @@ -72,86 +73,55 @@ void ApplyIblSpecular( outSpecular = lerp(outSpecular, probeSpecular, blendAmount); } + return outSpecular; } void ApplyIBL(Surface surface, inout LightingData lightingData) { - if (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent) +#ifdef FORCE_IBL_IN_FORWARD_PASS + bool useDiffuseIbl = true; + bool useSpecularIbl = true; + bool useIbl = o_enableIBL; +#else + bool isTransparent = (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent); + bool useDiffuseIbl = isTransparent; + bool useSpecularIbl = (isTransparent || o_meshUseForwardPassIBLSpecular || o_materialUseForwardPassIBLSpecular); + bool useIbl = o_enableIBL && (useDiffuseIbl || useSpecularIbl); +#endif + + if(useIbl) { - // transparencies currently require IBL in the forward pass - if (o_enableIBL) + float iblExposureFactor = pow(2.0, SceneSrg::m_iblExposure); + + if(useDiffuseIbl) { - float3 iblDiffuse = 0.0f; - ApplyIblDiffuse( - surface.normal, - surface.albedo, - lightingData.diffuseResponse, - iblDiffuse); - - float3 iblSpecular = 0.0f; - ApplyIblSpecular( - surface.position, - surface.normal, - surface.specularF0, - surface.roughnessLinear, - lightingData.dirToCamera, - lightingData.brdf, - iblSpecular); - - // Adjust IBL lighting by exposure. - float iblExposureFactor = pow(2.0, SceneSrg::m_iblExposure); + float3 iblDiffuse = GetIblDiffuse(surface.normal, surface.albedo, lightingData.diffuseResponse); lightingData.diffuseLighting += (iblDiffuse * iblExposureFactor * lightingData.diffuseAmbientOcclusion); - lightingData.specularLighting += (iblSpecular * iblExposureFactor); } - } - else if (o_meshUseForwardPassIBLSpecular || o_materialUseForwardPassIBLSpecular) - { - if (o_enableIBL) - { - float3 iblSpecular = 0.0f; - ApplyIblSpecular( - surface.position, - surface.normal, - surface.specularF0, - surface.roughnessLinear, - lightingData.dirToCamera, - lightingData.brdf, - iblSpecular); + if(useSpecularIbl) + { + float3 iblSpecular = GetIblSpecular(surface.position, surface.normal, surface.specularF0, surface.roughnessLinear, lightingData.dirToCamera, lightingData.brdf); iblSpecular *= lightingData.multiScatterCompensation; - if (o_clearCoat_feature_enabled) + if (o_clearCoat_feature_enabled && surface.clearCoat.factor > 0.0f) { - if (surface.clearCoat.factor > 0.0f) - { - float clearCoatNdotV = saturate(dot(surface.clearCoat.normal, lightingData.dirToCamera)); - clearCoatNdotV = max(clearCoatNdotV, 0.01f); // [GFX TODO][ATOM-4466] This is a current band-aid for specular noise at grazing angles. - float2 clearCoatBrdf = PassSrg::m_brdfMap.Sample(PassSrg::LinearSampler, GetBRDFTexCoords(surface.clearCoat.roughness, clearCoatNdotV)).rg; + float clearCoatNdotV = saturate(dot(surface.clearCoat.normal, lightingData.dirToCamera)); + clearCoatNdotV = max(clearCoatNdotV, 0.01f); // [GFX TODO][ATOM-4466] This is a current band-aid for specular noise at grazing angles. + float2 clearCoatBrdf = PassSrg::m_brdfMap.Sample(PassSrg::LinearSampler, GetBRDFTexCoords(surface.clearCoat.roughness, clearCoatNdotV)).rg; - // clear coat uses fixed IOR = 1.5 represents polyurethane which is the most common material for gloss clear coat - // coat layer assumed to be dielectric thus don't need multiple scattering compensation - float3 clearCoatSpecularF0 = float3(0.04f, 0.04f, 0.04f); - float3 clearCoatIblSpecular = 0.0f; + // clear coat uses fixed IOR = 1.5 represents polyurethane which is the most common material for gloss clear coat + // coat layer assumed to be dielectric thus don't need multiple scattering compensation + float3 clearCoatSpecularF0 = float3(0.04f, 0.04f, 0.04f); + float3 clearCoatIblSpecular = GetIblSpecular(surface.position, surface.clearCoat.normal, clearCoatSpecularF0, surface.clearCoat.roughness, lightingData.dirToCamera, clearCoatBrdf); - ApplyIblSpecular( - surface.position, - surface.clearCoat.normal, - clearCoatSpecularF0, - surface.clearCoat.roughness, - lightingData.dirToCamera, - clearCoatBrdf, - clearCoatIblSpecular); - - clearCoatIblSpecular *= surface.clearCoat.factor; + clearCoatIblSpecular *= surface.clearCoat.factor; - // attenuate base layer energy - float3 clearCoatResponse = FresnelSchlickWithRoughness(clearCoatNdotV, clearCoatSpecularF0, surface.clearCoat.roughness) * surface.clearCoat.factor; - iblSpecular = iblSpecular * (1.0 - clearCoatResponse) * (1.0 - clearCoatResponse) + clearCoatIblSpecular; - } + // attenuate base layer energy + float3 clearCoatResponse = FresnelSchlickWithRoughness(clearCoatNdotV, clearCoatSpecularF0, surface.clearCoat.roughness) * surface.clearCoat.factor; + iblSpecular = iblSpecular * (1.0 - clearCoatResponse) * (1.0 - clearCoatResponse) + clearCoatIblSpecular; } - - float iblExposureFactor = pow(2.0f, SceneSrg::m_iblExposure); lightingData.specularLighting += (iblSpecular * iblExposureFactor); } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli new file mode 100644 index 0000000000..cc4aa7cf42 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/ShaderQualityOptions.azsli @@ -0,0 +1,26 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +// This file translates quality option macros like QUALITY_LOW_END to their relevant settings + +#ifdef QUALITY_LOW_END + + // Unifies the forward output into a single lighting buffer instead of splitting it into a GBuffer + #define UNIFIED_FORWARD_OUTPUT 1 + + // Forces IBL lighting to be executed in the forward pass instead of subsequent refleciton passes + #define FORCE_IBL_IN_FORWARD_PASS 1 + +#endif + diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl index 1ee30a4f98..1bebb2ec47 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox.azsl @@ -10,6 +10,9 @@ * */ +// --- Static Options Available --- +// SKYBOX_TWO_OUTPUTS - Skybox renders to two rendertargets instead of one (SkyBox_TwoOutputs.pass writes to specular and reflection targets) + #include #include #include @@ -102,7 +105,9 @@ float3 GetCubemapCoords(float3 original) struct PSOutput { float4 m_specular : SV_Target0; +#ifdef SKYBOX_TWO_OUTPUTS float4 m_reflection : SV_Target1; +#endif }; PSOutput MainPS(VSOutput input) @@ -163,6 +168,8 @@ PSOutput MainPS(VSOutput input) PSOutput OUT; OUT.m_specular = float4(color, 1.0); +#ifdef SKYBOX_TWO_OUTPUTS OUT.m_reflection = float4(color, 1.0); +#endif return OUT; } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl new file mode 100644 index 0000000000..feacd2f44f --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.azsl @@ -0,0 +1,17 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +// NOTE: This file is a temporary workaround until .shader files can #define macros for their .azsl files + +#define SKYBOX_TWO_OUTPUTS + +#include "SkyBox.azsl" diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader new file mode 100644 index 0000000000..ec80d4a20e --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkyBox/SkyBox_TwoOutputs.shader @@ -0,0 +1,22 @@ +{ + "Source" : "SkyBox_TwoOutputs", + + "DepthStencilState" : { + "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" } + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "MainVS", + "type": "Vertex" + }, + { + "name": "MainPS", + "type": "Fragment" + } + ] + } +} 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 84ef494216..f1d8fa81be 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 @@ -38,6 +38,7 @@ set(FILES Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader Materials/Types/StandardMultilayerPBR_Parallax.lua Materials/Types/StandardMultilayerPBR_ParallaxPerLayer.lua + Materials/Types/StandardMultilayerPBR_ShaderEnable.lua Materials/Types/StandardMultilayerPBR_Shadowmap_WithPS.azsl Materials/Types/StandardMultilayerPBR_Shadowmap_WithPS.shader Materials/Types/StandardPBR.materialtype @@ -52,6 +53,9 @@ set(FILES Materials/Types/StandardPBR_ForwardPass_EDS.shader Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua Materials/Types/StandardPBR_HandleOpacityMode.lua + Materials/Types/StandardPBR_LowEndForward.azsl + Materials/Types/StandardPBR_LowEndForward.shader + Materials/Types/StandardPBR_LowEndForward_EDS.shader Materials/Types/StandardPBR_ParallaxState.lua Materials/Types/StandardPBR_Roughness.lua Materials/Types/StandardPBR_ShaderEnable.lua @@ -116,6 +120,7 @@ set(FILES Passes/DiffuseProbeGridBlendDistance.pass Passes/DiffuseProbeGridBlendIrradiance.pass Passes/DiffuseProbeGridBorderUpdate.pass + Passes/DiffuseProbeGridClassification.pass Passes/DiffuseProbeGridDownsample.pass Passes/DiffuseProbeGridRayTracing.pass Passes/DiffuseProbeGridRelocation.pass @@ -144,6 +149,7 @@ set(FILES Passes/FullscreenCopy.pass Passes/FullscreenOutputOnly.pass Passes/ImGui.pass + Passes/LightAdaptationParent.pass Passes/LightCulling.pass Passes/LightCullingHeatmap.pass Passes/LightCullingParent.pass @@ -152,6 +158,8 @@ set(FILES Passes/LightCullingTilePrepareMSAA.pass Passes/LookModificationComposite.pass Passes/LookModificationTransform.pass + Passes/LowEndForward.pass + Passes/LowEndPipeline.pass Passes/LuminanceHeatmap.pass Passes/LuminanceHistogramGenerator.pass Passes/MainPipeline.pass @@ -179,13 +187,16 @@ set(FILES Passes/ReflectionScreenSpace.pass Passes/ReflectionScreenSpaceBlur.pass Passes/ReflectionScreenSpaceBlurHorizontal.pass + Passes/ReflectionScreenSpaceBlurMobile.pass Passes/ReflectionScreenSpaceBlurVertical.pass Passes/ReflectionScreenSpaceComposite.pass + Passes/ReflectionScreenSpaceMobile.pass Passes/ReflectionScreenSpaceTrace.pass Passes/Reflections_nomsaa.pass Passes/ShadowParent.pass Passes/Skinning.pass Passes/SkyBox.pass + Passes/SkyBox_TwoOutputs.pass Passes/SMAA1xApplyLinearHDRColor.pass Passes/SMAA1xApplyPerceptualColor.pass Passes/SMAABlendingWeightCalculation.pass @@ -205,6 +216,7 @@ set(FILES ShaderLib/Atom/Features/IndirectRendering.azsli ShaderLib/Atom/Features/MatrixUtility.azsli ShaderLib/Atom/Features/ParallaxMapping.azsli + ShaderLib/Atom/Features/ShaderQualityOptions.azsli ShaderLib/Atom/Features/SphericalHarmonicsUtility.azsli ShaderLib/Atom/Features/SrgSemantics.azsli ShaderLib/Atom/Features/ColorManagement/TransformColor.azsli @@ -272,6 +284,7 @@ set(FILES ShaderLib/Atom/Features/PostProcessing/GlyphData.azsli ShaderLib/Atom/Features/PostProcessing/GlyphRender.azsli ShaderLib/Atom/Features/PostProcessing/PostProcessUtil.azsli + ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli ShaderLib/Atom/Features/ScreenSpace/ScreenSpaceUtil.azsli ShaderLib/Atom/Features/Shadow/BicubicPcfFilters.azsli ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli @@ -471,4 +484,6 @@ set(FILES Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli Shaders/SkyBox/SkyBox.azsl Shaders/SkyBox/SkyBox.shader + Shaders/SkyBox/SkyBox_TwoOutputs.azsl + Shaders/SkyBox/SkyBox_TwoOutputs.shader ) diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h index 7875e38fc0..0d61ef82d1 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h @@ -148,6 +148,8 @@ namespace AZ Data::Instance GetModel(const MeshHandle& meshHandle) const override; Data::Asset GetModelAsset(const MeshHandle& meshHandle) const override; + Data::Instance GetObjectSrg(const MeshHandle& meshHandle) const override; + void QueueObjectSrgForCompile(const MeshHandle& meshHandle) const override; void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material) override; void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const MaterialAssignmentMap& materials) override; const MaterialAssignmentMap& GetMaterialAssignmentMap(const MeshHandle& meshHandle) const override; diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h index c2360068de..fb5bff5584 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessorInterface.h @@ -61,6 +61,14 @@ namespace AZ virtual Data::Instance GetModel(const MeshHandle& meshHandle) const = 0; //! Gets the underlying RPI::ModelAsset for a meshHandle. virtual Data::Asset GetModelAsset(const MeshHandle& meshHandle) const = 0; + //! Gets the ObjectSrg for a meshHandle. + //! Updating the ObjectSrg should be followed by a call to QueueObjectSrgForCompile, + //! instead of compiling the srg directly. This way, if the srg has already been queued for compile, + //! it will not be queued twice in the same frame. The ObjectSrg should not be updated during + //! Simulate, or it will create a race between updating the data and the call to Compile + virtual Data::Instance GetObjectSrg(const MeshHandle& meshHandle) const = 0; + //! Queues the object srg for compile. + virtual void QueueObjectSrgForCompile(const MeshHandle& meshHandle) const = 0; //! Sets the MaterialAssignmentMap for a meshHandle, using just a single material for the DefaultMaterialAssignmentId. //! Note if there is already a material assignment map, this will replace the entire map with just a single material. virtual void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material) = 0; diff --git a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h index 39fd7b4380..418ee0cfb8 100644 --- a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h @@ -23,6 +23,8 @@ namespace UnitTest MOCK_METHOD1(CloneMesh, MeshHandle(const MeshHandle&)); MOCK_CONST_METHOD1(GetModel, AZStd::intrusive_ptr(const MeshHandle&)); MOCK_CONST_METHOD1(GetModelAsset, AZ::Data::Asset(const MeshHandle&)); + MOCK_CONST_METHOD1(GetObjectSrg, AZStd::intrusive_ptr(const MeshHandle&)); + MOCK_CONST_METHOD1(QueueObjectSrgForCompile, void(const MeshHandle&)); MOCK_CONST_METHOD1(GetMaterialAssignmentMap, const AZ::Render::MaterialAssignmentMap&(const MeshHandle&)); MOCK_METHOD2(ConnectModelChangeEventHandler, void(const MeshHandle&, ModelChangedEvent::Handler&)); MOCK_METHOD3(SetTransform, void(const MeshHandle&, const AZ::Transform&, const AZ::Vector3&)); diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index 29f0636e9e..4059d65cbb 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -231,6 +231,19 @@ namespace AZ return {}; } + Data::Instance MeshFeatureProcessor::GetObjectSrg(const MeshHandle& meshHandle) const + { + return meshHandle.IsValid() ? meshHandle->m_shaderResourceGroup : nullptr; + } + + void MeshFeatureProcessor::QueueObjectSrgForCompile(const MeshHandle& meshHandle) const + { + if (meshHandle.IsValid()) + { + meshHandle->m_objectSrgNeedsUpdate = true; + } + } + void MeshFeatureProcessor::SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance& material) { Render::MaterialAssignmentMap materials; diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp index 388f4112a0..cb2d6a69ef 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp @@ -71,16 +71,6 @@ namespace AZ } - void SkinnedMeshFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) - { - AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); - AZ_ATOM_PROFILE_FUNCTION("SkinnedMesh", "SkinnedMeshFeatureProcessor: Simulate"); - AZ_UNUSED(packet); - - SkinnedMeshFeatureProcessorNotificationBus::Broadcast(&SkinnedMeshFeatureProcessorNotificationBus::Events::OnUpdateSkinningMatrices); - - } - void SkinnedMeshFeatureProcessor::Render(const FeatureProcessor::RenderPacket& packet) { AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); @@ -268,6 +258,8 @@ namespace AZ void SkinnedMeshFeatureProcessor::OnBeginPrepareRender() { m_renderProxiesChecker.soft_lock(); + + SkinnedMeshFeatureProcessorNotificationBus::Broadcast(&SkinnedMeshFeatureProcessorNotificationBus::Events::OnUpdateSkinningMatrices); } void SkinnedMeshFeatureProcessor::OnRenderEnd() diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h index bb3dd242a1..75d41742b2 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h @@ -49,7 +49,6 @@ namespace AZ // FeatureProcessor overrides ... void Activate() override; void Deactivate() override; - void Simulate(const FeatureProcessor::SimulatePacket& packet) override; void Render(const FeatureProcessor::RenderPacket& packet) override; void OnRenderEnd() override; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h index b0d6bd4117..1cba71ae7e 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h @@ -381,6 +381,7 @@ namespace AZ uint64_t m_createdByPassRequest : 1; uint64_t m_initialized : 1; uint64_t m_enabled : 1; + uint64_t m_parentEnabled : 1; uint64_t m_alreadyCreated : 1; uint64_t m_alreadyReset : 1; uint64_t m_alreadyPrepared : 1; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h index 5b92047226..4aa3faa6c9 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/MorphTargetMetaAsset.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace AZ::RPI { @@ -56,6 +57,9 @@ namespace AZ::RPI float m_minPositionDelta; float m_maxPositionDelta; + //! Reference to the wrinkle mask, if it exists + AZ::Data::Asset m_wrinkleMask; + //! Boolean to indicate the presence or absence of color deltas bool m_hasColorDeltas = false; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp index 7aace50760..3d0cbca8e6 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.cpp @@ -18,6 +18,9 @@ #include #include +#include +#include + namespace AZ::RPI { using namespace AZ::SceneAPI; @@ -114,7 +117,7 @@ namespace AZ::RPI meshNodeName, sourceMesh.m_name.GetCStr()); const DataTypes::MatrixType globalTransform = Utilities::BuildWorldTransform(sceneGraph, sceneNodeIndex); - BuildMorphTargetMesh(vertexOffset, sourceMesh, productMesh, metaAssetCreator, blendShapeName, blendShapeData, globalTransform, coordSysConverter); + BuildMorphTargetMesh(vertexOffset, sourceMesh, productMesh, metaAssetCreator, blendShapeName, blendShapeData, globalTransform, coordSysConverter, scene.GetSourceFilename()); } } } @@ -157,7 +160,8 @@ namespace AZ::RPI const AZStd::string& blendShapeName, const AZStd::shared_ptr& blendShapeData, const DataTypes::MatrixType& globalTransform, - const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter) + const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter, + const AZStd::string& sourceSceneFilename) { const float tolerance = CalcPositionDeltaTolerance(sourceMesh); AZ::Aabb deltaPositionAabb = AZ::Aabb::CreateNull(); @@ -288,6 +292,8 @@ namespace AZ::RPI metaData.m_maxPositionDelta = maxValue; } + metaData.m_wrinkleMask = GetWrinkleMask(sourceSceneFilename, blendShapeName); + metaAssetCreator.AddMorphTarget(metaData); AZ_Assert(uncompressedPositionDeltas.size() == compressedDeltas.size(), "Number of uncompressed (%d) and compressed position delta components (%d) do not match.", @@ -312,4 +318,47 @@ namespace AZ::RPI AZ_Assert((packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex) == numMorphedVertices, "Vertex index range (%d) in morph target meta data does not match number of morphed vertices (%d).", packedCompressedMorphTargetVertexData.size() - metaData.m_startIndex, numMorphedVertices); } + + Data::Asset MorphTargetExporter::GetWrinkleMask(const AZStd::string& sourceSceneFullFilePath, const AZStd::string& blendShapeName) const + { + AZ::Data::Asset imageAsset; + + // See if there is a wrinkle map mask for this mesh + AZStd::string sceneRelativeFilePath; + bool relativePathFound = true; + AzToolsFramework::AssetSystemRequestBus::BroadcastResult(relativePathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetRelativeProductPathFromFullSourceOrProductPath, sourceSceneFullFilePath, sceneRelativeFilePath); + + if (relativePathFound) + { + AZ::StringFunc::Path::StripFullName(sceneRelativeFilePath); + + // Get the folder the masks are supposed to be in + AZStd::string folderName; + AZ::StringFunc::Path::GetFileName(sourceSceneFullFilePath.c_str(), folderName); + folderName += "_wrinklemasks"; + + // Note: for now, we're assuming the mask is always authored as a .tif + AZStd::string blendMaskFileName = blendShapeName + "_wrinklemask.tif.streamingimage"; + + AZStd::string maskFolderAndFile; + AZ::StringFunc::Path::Join(folderName.c_str(), blendMaskFileName.c_str(), maskFolderAndFile); + + AZStd::string maskRelativePath; + AZ::StringFunc::Path::Join(sceneRelativeFilePath.c_str(), maskFolderAndFile.c_str(), maskRelativePath); + AZ::StringFunc::Path::Normalize(maskRelativePath); + + // Now see if the file exists + AZ::Data::AssetId maskAssetId; + Data::AssetCatalogRequestBus::BroadcastResult(maskAssetId, &Data::AssetCatalogRequests::GetAssetIdByPath, maskRelativePath.c_str(), AZ::Data::s_invalidAssetType, false); + + if (maskAssetId.IsValid()) + { + // Flush asset manager events to ensure no asset references are held by closures queued on Ebuses. + AZ::Data::AssetManager::Instance().DispatchEvents(); + + imageAsset.Create(maskAssetId, AZ::Data::AssetLoadBehavior::PreLoad, false); + } + } + return imageAsset; + } } // namespace AZ::RPI diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h index d968a803d6..4845d7d1da 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Model/MorphTargetExporter.h @@ -64,7 +64,11 @@ namespace AZ const AZStd::string& blendShapeName, const AZStd::shared_ptr& blendShapeData, const AZ::SceneAPI::DataTypes::MatrixType& globalTransform, - const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter); + const AZ::SceneAPI::CoordinateSystemConverter& coordSysConverter, + const AZStd::string& sourceSceneFilename); + + // Find a wrinkle mask for this morph target, if it exists + Data::Asset GetWrinkleMask(const AZStd::string& sourceSceneFullFilePath, const AZStd::string& blendShapeName) const; }; } // namespace RPI } // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp index 2ab6ee92e9..109af70166 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp @@ -422,7 +422,7 @@ namespace AZ const MaterialPropertyDescriptor* propertyDescriptor = materialTypeAssetCreator.GetMaterialPropertiesLayout()->GetPropertyDescriptor(propertyIndex); AZ::Name enumName = AZ::Name(property.m_value.GetValue()); - uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName); + uint32_t enumValue = propertyDescriptor ? propertyDescriptor->GetEnumValue(enumName) : MaterialPropertyDescriptor::InvalidEnumValue; if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue) { materialTypeAssetCreator.ReportError("Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr()); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp index 9401d1a9e0..6ed8ac018c 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp @@ -93,11 +93,12 @@ namespace AZ void Pass::SetEnabled(bool enabled) { m_flags.m_enabled = enabled; + OnHierarchyChange(); } bool Pass::IsEnabled() const { - return m_flags.m_enabled; + return m_flags.m_enabled && (m_flags.m_parentEnabled || m_parent == nullptr); } // --- Error Logging --- @@ -140,6 +141,7 @@ namespace AZ } // Set new tree depth and path + m_flags.m_parentEnabled = m_parent->m_flags.m_enabled && (m_parent->m_flags.m_parentEnabled || m_parent->m_parent == nullptr); m_treeDepth = m_parent->m_treeDepth + 1; m_path = ConcatPassName(m_parent->m_path, m_name); m_flags.m_partOfHierarchy = m_parent->m_flags.m_partOfHierarchy; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp index 1fad385fa6..3f5c16678c 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp @@ -163,6 +163,11 @@ namespace AZ binding.m_shaderInputIndex = idx.IsValid() ? static_cast(idx.GetIndex()) : PassAttachmentBinding::ShaderInputNoBind; } } + else + { + AZ_Error("Pass System", false, "[Pass %s] Could not bind shader buffer index '%s' because it has no attachment.", GetName().GetCStr(), shaderName.GetCStr()); + binding.m_shaderInputIndex = PassAttachmentBinding::ShaderInputNoBind; + } } } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp index 313e0bea31..3c0f832807 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/MorphTargetMetaAsset.cpp @@ -28,6 +28,7 @@ namespace AZ::RPI ->Field("numVertices", &MorphTargetMetaAsset::MorphTarget::m_numVertices) ->Field("minPositionDelta", &MorphTargetMetaAsset::MorphTarget::m_minPositionDelta) ->Field("maxPositionDelta", &MorphTargetMetaAsset::MorphTarget::m_maxPositionDelta) + ->Field("wrinkleMask", &MorphTargetMetaAsset::MorphTarget::m_wrinkleMask) ->Field("hasColorDeltas", &MorphTargetMetaAsset::MorphTarget::m_hasColorDeltas) ; } diff --git a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material index f43f6d0808..c359fea3b5 100644 --- a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material +++ b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/001_lucy_regression_test.material @@ -11,7 +11,7 @@ 0.29372090101242068, 1.0 ], - "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_BaseColor.png", "useTexture": false }, "detailLayerGroup": { @@ -30,7 +30,7 @@ }, "normal": { "flipY": true, - "textureMap": "Objects/Lucy/Lucy_normal.tif" + "textureMap": "Objects/Lucy/Lucy_normal.png" }, "subsurfaceScattering": { "enableSubsurfaceScattering": true, diff --git a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material index c611b992b6..ce42f32b67 100644 --- a/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material +++ b/Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material @@ -29,7 +29,7 @@ }, "normal": { "flipY": true, - "textureMap": "Objects/Lucy/Lucy_normal.tif" + "textureMap": "Objects/Lucy/Lucy_normal.png" }, "subsurfaceScattering": { "enableSubsurfaceScattering": true, diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material index 2c711a3bf3..7b1f0ba6a9 100644 --- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material +++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/101_DetailMaps_LucyBaseNoDetailMaps.material @@ -5,20 +5,20 @@ "propertyLayoutVersion": 3, "properties": { "baseColor": { - "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_BaseColor.png", "textureMapUv": "Unwrapped" }, "metallic": { - "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png", "textureMapUv": "Unwrapped" }, "normal": { "flipY": true, - "textureMap": "Objects/Lucy/Lucy_normal.tif", + "textureMap": "Objects/Lucy/Lucy_normal.png", "textureMapUv": "Unwrapped" }, "roughness": { - "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png", "textureMapUv": "Unwrapped" } } diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material index 7a94386a18..55a01866b5 100644 --- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material +++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/102_DetailMaps_All.material @@ -5,7 +5,7 @@ "propertyLayoutVersion": 3, "properties": { "baseColor": { - "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_baseColor.png", "textureMapUv": "Unwrapped" }, "detailLayerGroup": { @@ -22,16 +22,16 @@ "scale": 10.0 }, "metallic": { - "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png", "textureMapUv": "Unwrapped" }, "normal": { "flipY": true, - "textureMap": "Objects/Lucy/Lucy_normal.tif", + "textureMap": "Objects/Lucy/Lucy_normal.png", "textureMapUv": "Unwrapped" }, "roughness": { - "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png", "textureMapUv": "Unwrapped" } } diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material index ddeace43da..6193cf4eed 100644 --- a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material +++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/105_DetailMaps_BlendMaskUsingDetailUVs.material @@ -5,7 +5,7 @@ "propertyLayoutVersion": 3, "properties": { "baseColor": { - "textureMap": "Objects/Lucy/Lucy_brass_baseColor.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_baseColor.png", "textureMapUv": "Unwrapped" }, "detailLayerGroup": { @@ -21,16 +21,16 @@ "scale": 10.0 }, "metallic": { - "textureMap": "Objects/Lucy/Lucy_brass_metalness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_metallic.png", "textureMapUv": "Unwrapped" }, "normal": { "flipY": true, - "textureMap": "Objects/Lucy/Lucy_normal.tif", + "textureMap": "Objects/Lucy/Lucy_normal.png", "textureMapUv": "Unwrapped" }, "roughness": { - "textureMap": "Objects/Lucy/Lucy_brass_roughness.tif", + "textureMap": "Objects/Lucy/Lucy_bronze_roughness.png", "textureMapUv": "Unwrapped" } } diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp index d0116452b7..9079f639ba 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -39,6 +40,8 @@ namespace AZ { namespace Render { + static constexpr uint32_t s_maxActiveWrinkleMasks = 16; + AZ_CLASS_ALLOCATOR_IMPL(AtomActorInstance, EMotionFX::Integration::EMotionFXAllocator, 0) AtomActorInstance::AtomActorInstance(AZ::EntityId entityId, @@ -413,6 +416,10 @@ namespace AZ EMotionFX::MorphSetup* morphSetup = m_actorInstance->GetActor()->GetMorphSetup(lodIndex); if (morphSetup) { + // Track all the masks/weights that are currently active + m_wrinkleMasks.clear(); + m_wrinkleMaskWeights.clear(); + uint32_t morphTargetCount = morphSetup->GetNumMorphTargets(); m_morphTargetWeights.clear(); for (uint32_t morphTargetIndex = 0; morphTargetIndex < morphTargetCount; ++morphTargetIndex) @@ -437,11 +444,28 @@ namespace AZ const EMotionFX::MorphTargetStandard::DeformData* deformData = morphTargetStandard->GetDeformData(deformDataIndex); if (deformData->mNumVerts > 0) { - m_morphTargetWeights.push_back(morphTargetSetupInstance->GetWeight()); + float weight = morphTargetSetupInstance->GetWeight(); + m_morphTargetWeights.push_back(weight); + + // If the morph target is active and it has a wrinkle mask + auto wrinkleMaskIter = m_morphTargetWrinkleMaskMapsByLod[lodIndex].find(morphTargetStandard); + if (weight > 0 && wrinkleMaskIter != m_morphTargetWrinkleMaskMapsByLod[lodIndex].end()) + { + // Add the wrinkle mask and weight, to be set on the material + m_wrinkleMasks.push_back(wrinkleMaskIter->second); + m_wrinkleMaskWeights.push_back(weight); + } } } } m_skinnedMeshRenderProxy->SetMorphTargetWeights(lodIndex, m_morphTargetWeights); + + // Until EMotionFX and Atom lods are synchronized [ATOM-13564] we don't know which EMotionFX lod to pull the weights from + // Until that is fixed, just use lod 0 [ATOM-15251] + if (lodIndex == 0) + { + UpdateWrinkleMasks(); + } } } } @@ -453,6 +477,8 @@ namespace AZ MaterialComponentRequestBus::EventResult(materials, m_entityId, &MaterialComponentRequests::GetMaterialOverrides); CreateRenderProxy(materials); + InitWrinkleMasks(); + TransformNotificationBus::Handler::BusConnect(m_entityId); MaterialComponentNotificationBus::Handler::BusConnect(m_entityId); MeshComponentRequestBus::Handler::BusConnect(m_entityId); @@ -573,5 +599,77 @@ namespace AZ { CreateSkinnedMeshInstance(); } + + void AtomActorInstance::InitWrinkleMasks() + { + EMotionFX::Actor* actor = m_actorAsset->GetActor(); + m_morphTargetWrinkleMaskMapsByLod.resize(m_skinnedMeshInputBuffers->GetLodCount()); + m_wrinkleMasks.reserve(s_maxActiveWrinkleMasks); + m_wrinkleMaskWeights.reserve(s_maxActiveWrinkleMasks); + + for (size_t lodIndex = 0; lodIndex < m_skinnedMeshInputBuffers->GetLodCount(); ++lodIndex) + { + EMotionFX::MorphSetup* morphSetup = actor->GetMorphSetup(lodIndex); + if (morphSetup) + { + const AZStd::vector& metaDatas = actor->GetMorphTargetMetaAsset()->GetMorphTargets(); + // Loop over all the EMotionFX morph targets + uint32_t numMorphTargets = morphSetup->GetNumMorphTargets(); + for (uint32_t morphTargetIndex = 0; morphTargetIndex < numMorphTargets; ++morphTargetIndex) + { + EMotionFX::MorphTargetStandard* morphTarget = static_cast(morphSetup->GetMorphTarget(morphTargetIndex)); + for (const RPI::MorphTargetMetaAsset::MorphTarget& metaData : metaDatas) + { + // Find the metaData associated with this morph target + if (metaData.m_morphTargetName == morphTarget->GetNameString() && metaData.m_wrinkleMask && metaData.m_numVertices > 0) + { + // If the metaData has a wrinkle mask, add it to the map + Data::Instance streamingImage = RPI::StreamingImage::FindOrCreate(metaData.m_wrinkleMask); + if (streamingImage) + { + m_morphTargetWrinkleMaskMapsByLod[lodIndex][morphTarget] = streamingImage; + } + } + } + } + } + } + } + + void AtomActorInstance::UpdateWrinkleMasks() + { + if (m_meshHandle) + { + Data::Instance wrinkleMaskObjectSrg = m_meshFeatureProcessor->GetObjectSrg(*m_meshHandle); + if (wrinkleMaskObjectSrg) + { + RHI::ShaderInputImageIndex wrinkleMasksIndex = wrinkleMaskObjectSrg->FindShaderInputImageIndex(Name{ "m_wrinkle_masks" }); + RHI::ShaderInputConstantIndex wrinkleMaskWeightsIndex = wrinkleMaskObjectSrg->FindShaderInputConstantIndex(Name{ "m_wrinkle_mask_weights" }); + RHI::ShaderInputConstantIndex wrinkleMaskCountIndex = wrinkleMaskObjectSrg->FindShaderInputConstantIndex(Name{ "m_wrinkle_mask_count" }); + if (wrinkleMasksIndex.IsValid() || wrinkleMaskWeightsIndex.IsValid() || wrinkleMaskCountIndex.IsValid()) + { + AZ_Error("AtomActorInstance", wrinkleMasksIndex.IsValid(), "m_wrinkle_masks not found on the ObjectSrg, but m_wrinkle_mask_weights and/or m_wrinkle_mask_count are being used."); + AZ_Error("AtomActorInstance", wrinkleMaskWeightsIndex.IsValid(), "m_wrinkle_mask_weights not found on the ObjectSrg, but m_wrinkle_masks and/or m_wrinkle_mask_count are being used."); + AZ_Error("AtomActorInstance", wrinkleMaskCountIndex.IsValid(), "m_wrinkle_mask_count not found on the ObjectSrg, but m_wrinkle_mask_weights and/or m_wrinkle_masks are being used."); + + if (m_wrinkleMasks.size()) + { + wrinkleMaskObjectSrg->SetImageArray(wrinkleMasksIndex, AZStd::array_view>(m_wrinkleMasks.data(), m_wrinkleMasks.size())); + + // Set the weights for any active masks + for (size_t i = 0; i < m_wrinkleMaskWeights.size(); ++i) + { + wrinkleMaskObjectSrg->SetConstant(wrinkleMaskWeightsIndex, m_wrinkleMaskWeights[i], i); + } + AZ_Error("AtomActorInstance", m_wrinkleMaskWeights.size() <= s_maxActiveWrinkleMasks, "The skinning shader supports no more than %d active morph targets with wrinkle masks.", s_maxActiveWrinkleMasks); + } + + wrinkleMaskObjectSrg->SetConstant(wrinkleMaskCountIndex, aznumeric_cast(m_wrinkleMasks.size())); + m_meshFeatureProcessor->QueueObjectSrgForCompile(*m_meshHandle); + } + } + } + } + } //namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h index 1002fcbde1..e05280e896 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.h @@ -17,6 +17,7 @@ #include #include +#include #include @@ -29,6 +30,8 @@ #include #include #include +#include + #include #include @@ -41,6 +44,7 @@ namespace AZ::RPI { class Model; class Buffer; + class StreamingImage; } namespace AZ @@ -168,6 +172,11 @@ namespace AZ // SkinnedMeshOutputStreamNotificationBus void OnSkinnedMeshOutputStreamMemoryAvailable() override; + // Check to see if the skin material is being used, + // and if there are blend shapes with wrinkle masks that should be applied to it + void InitWrinkleMasks(); + void UpdateWrinkleMasks(); + AZStd::intrusive_ptr m_skinnedMeshInputBuffers = nullptr; AZStd::intrusive_ptr m_skinnedMeshInstance; AZ::Data::Instance m_boneTransforms = nullptr; @@ -179,6 +188,12 @@ namespace AZ AZ::TransformInterface* m_transformInterface = nullptr; AZStd::set m_waitForMaterialLoadIds; AZStd::vector m_morphTargetWeights; + + typedef AZStd::unordered_map> MorphTargetWrinkleMaskMap; + AZStd::vector m_morphTargetWrinkleMaskMapsByLod; + + AZStd::vector> m_wrinkleMasks; + AZStd::vector m_wrinkleMaskWeights; }; } // namespace Render diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 7a4eaeb014..019f341d0c 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -96,12 +96,12 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) PRIVATE Gem::Multiplayer.Tools.Static ) - + ly_add_target( - NAME Multiplayer.Editor.Static STATIC + NAME Multiplayer.Editor GEM_MODULE NAMESPACE Gem FILES_CMAKE - multiplayer_editor_files.cmake + multiplayer_editor_shared_files.cmake COMPILE_DEFINITIONS PUBLIC MULTIPLAYER_EDITOR @@ -113,7 +113,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) PUBLIC Include BUILD_DEPENDENCIES - PUBLIC + PRIVATE Legacy::CryCommon Legacy::Editor.Headers AZ::AzCore @@ -121,23 +121,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) AZ::AzNetworking AZ::AzToolsFramework Gem::Multiplayer.Static - ) - - ly_add_target( - NAME Multiplayer.Editor GEM_MODULE - NAMESPACE Gem - FILES_CMAKE - multiplayer_editor_shared_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - . - Source - ${pal_source_dir} - PUBLIC - Include - BUILD_DEPENDENCIES - PRIVATE - Gem::Multiplayer.Editor.Static + Gem::Multiplayer.Tools ) endif() diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h new file mode 100644 index 0000000000..c621808f7a --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h @@ -0,0 +1,39 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include + +namespace Multiplayer +{ + //! IMultiplayer provides insight into the Multiplayer session and its Agents + class IMultiplayerTools + { + public: + // NetworkPrefabProcessor is the only class that should be setting process network prefab status + friend class NetworkPrefabProcessor; + + AZ_RTTI(IMultiplayerTools, "{E8A80EAB-29CB-4E3B-A0B2-FFCB37060FB0}"); + + virtual ~IMultiplayerTools() = default; + + //! Returns if network prefab processing has created currently active or pending spawnables + //! @return If network prefab processing has created currently active or pending spawnables + virtual bool DidProcessNetworkPrefabs() = 0; + + private: + //! Sets if network prefab processing has created currently active or pending spawnables + //! @param didProcessNetPrefabs if network prefab processing has created currently active or pending spawnables + virtual void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) = 0; + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h new file mode 100644 index 0000000000..b82fab91be --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h @@ -0,0 +1,32 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + constexpr AZStd::string_view MPNetworkInterfaceName("MultiplayerNetworkInterface"); + constexpr AZStd::string_view MPEditorInterfaceName("MultiplayerEditorNetworkInterface"); + + constexpr AZStd::string_view LocalHost("127.0.0.1"); + constexpr uint16_t DefaultServerPort = 30090; + constexpr uint16_t DefaultServerEditorPort = 30091; + +} + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 2f934979b1..a1e68aa708 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -59,4 +59,5 @@ + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml new file mode 100644 index 0000000000..8f55ecd2b8 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 10a0d17e73..612601883c 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -23,7 +23,7 @@ namespace Multiplayer { AZ_CVAR(AZ::TimeMs, cl_InputRateMs, AZ::TimeMs{ 33 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Rate at which to sample and process client inputs"); AZ_CVAR(AZ::TimeMs, cl_MaxRewindHistoryMs, AZ::TimeMs{ 2000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of milliseconds to keep for server correction rewind and replay"); -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD AZ_CVAR(float, cl_DebugHackTimeMultiplier, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Scalar value used to simulate clock hacking cheats for validating bank time system and anticheat"); #endif @@ -477,7 +477,7 @@ namespace Multiplayer const double inputRate = static_cast(static_cast(cl_InputRateMs)) / 1000.0; const double maxRewindHistory = static_cast(static_cast(cl_MaxRewindHistoryMs)) / 1000.0; -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD m_moveAccumulator += deltaTime * cl_DebugHackTimeMultiplier; #else m_moveAccumulator += deltaTime; diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp new file mode 100644 index 0000000000..f684e1f12f --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -0,0 +1,187 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + using namespace AzNetworking; + + AZ_CVAR(bool, editorsv_isDedicated, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether to init as a server expecting data from an Editor. Do not modify unless you're sure of what you're doing."); + + MultiplayerEditorConnection::MultiplayerEditorConnection() + : m_byteStream(&m_buffer) + { + m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( + AZ::Name(MPEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); + if (editorsv_isDedicated) + { + uint16_t editorServerPort = DefaultServerEditorPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("editorsv_port", editorServerPort); + } + AZ_Assert(m_networkEditorInterface, "MP Editor Network Interface was unregistered before Editor Server could start listening."); + m_networkEditorInterface->Listen(editorServerPort); + } + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerInit& packet + ) + { + // Editor Server Init is intended for non-release targets + if (!packet.GetLastUpdate()) + { + // More packets are expected, flush this to the buffer + m_byteStream.Write(TcpPacketEncodingBuffer::GetCapacity(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + } + else + { + // This is the last expected packet, flush it to the buffer + m_byteStream.Write(packet.GetAssetData().GetSize(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + + // Read all assets out of the buffer + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + AZStd::vector> assetData; + while (m_byteStream.GetCurPos() < m_byteStream.GetLength()) + { + AZ::Data::AssetId assetId; + uint32_t hintSize; + AZStd::string assetHint; + m_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + m_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); + assetHint.resize(hintSize); + m_byteStream.Read(hintSize, assetHint.data()); + + size_t assetSize = m_byteStream.GetCurPos(); + AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(m_byteStream, nullptr); + assetSize = m_byteStream.GetCurPos() - assetSize; + AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, AZ::Data::AssetLoadBehavior::NoLoad); + asset.SetHint(assetHint); + + AZ::Data::AssetInfo assetInfo; + assetInfo.m_assetId = asset.GetId(); + assetInfo.m_assetType = asset.GetType(); + assetInfo.m_relativePath = asset.GetHint(); + assetInfo.m_sizeBytes = assetSize; + + // Register Asset to AssetManager + AZ::Data::AssetManager::Instance().AssignAssetData(asset); + AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequests::RegisterAsset, asset.GetId(), assetInfo); + + assetData.push_back(asset); + } + + // Now that we've deserialized, clear the byte stream + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + m_byteStream.Truncate(); + + // Load the level via the root spawnable that was registered + const AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + + // Setup the normal multiplayer connection + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + uint16_t serverPort = DefaultServerPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("sv_port", serverPort); + } + networkInterface->Listen(serverPort); + + AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); + return connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady()); + } + + return true; + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerReady& packet + ) + { + if (connection->GetConnectionRole() == ConnectionRole::Connector) + { + // Receiving this packet means Editor sync is done, disconnect + connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); + + if (auto console = AZ::Interface::Get(); console) + { + AZ::CVarFixedString remoteAddress; + uint16_t remotePort; + if (console->GetCvarValue("editorsv_serveraddr", remoteAddress) != AZ::GetValueResult::ConsoleVarNotFound && + console->GetCvarValue("editorsv_port", remotePort) != AZ::GetValueResult::ConsoleVarNotFound) + { + // Connect the Editor to the editor server for Multiplayer simulation + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + INetworkInterface* networkInterface = + AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + const IpAddress ipAddress(remoteAddress.c_str(), remotePort, networkInterface->GetType()); + networkInterface->Connect(ipAddress); + } + } + } + return true; + } + + ConnectResult MultiplayerEditorConnection::ValidateConnect + ( + [[maybe_unused]] const IpAddress& remoteAddress, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] ISerializer& serializer + ) + { + return ConnectResult::Accepted; + } + + void MultiplayerEditorConnection::OnConnect([[maybe_unused]] AzNetworking::IConnection* connection) + { + ; + } + + bool MultiplayerEditorConnection::OnPacketReceived(AzNetworking::IConnection* connection, const IPacketHeader& packetHeader, ISerializer& serializer) + { + return MultiplayerEditorPackets::DispatchPacket(connection, packetHeader, serializer, *this); + } + + void MultiplayerEditorConnection::OnPacketLost([[maybe_unused]] IConnection* connection, [[maybe_unused]] PacketId packetId) + { + ; + } + + void MultiplayerEditorConnection::OnDisconnect([[maybe_unused]] AzNetworking::IConnection* connection, [[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) + { + ; + } +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h new file mode 100644 index 0000000000..d803a60744 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -0,0 +1,58 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace AzNetworking +{ + class INetworkInterface; +} + +namespace Multiplayer +{ + //! MultiplayerEditorConnection is a connection listener to synchronize the Editor and a local server it launches + class MultiplayerEditorConnection final + : public AzNetworking::IConnectionListener + { + public: + MultiplayerEditorConnection(); + ~MultiplayerEditorConnection() = default; + + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerInit& packet); + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerReady& packet); + + //! IConnectionListener interface + //! @{ + AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnConnect(AzNetworking::IConnection* connection) override; + bool OnPacketReceived(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnPacketLost(AzNetworking::IConnection* connection, AzNetworking::PacketId packetId) override; + void OnDisconnect(AzNetworking::IConnection* connection, AzNetworking::DisconnectReason reason, AzNetworking::TerminationEndpoint endpoint) override; + //! @} + + private: + + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; + AZStd::vector m_buffer; + AZ::IO::ByteContainerStream> m_byteStream; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp deleted file mode 100644 index 470aa61cd0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* - * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or - * its licensors. - * - * For complete copyright and license terms please see the LICENSE at the root of this - * distribution (the "License"). All use of this software is governed by the License, - * or, if provided, by the license below or the license accompanying this file. Do not - * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - */ - -#include - -namespace Multiplayer -{ - MultiplayerEditorDispatcher::MultiplayerEditorDispatcher() - { - ; - } -} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h deleted file mode 100644 index c1058dc8a0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#pragma once - -#include - -#include -#include -#include -#include - -#include - - -namespace Multiplayer -{ - //! MultiplayerEditorDispatcher is responsible for dispatching delta from the Editor to an Editor launched local server - class MultiplayerEditorDispatcher final - { - public: - MultiplayerEditorDispatcher(); - ~MultiplayerEditorDispatcher() = default; - - private: - }; -} diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp similarity index 86% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp index 97c5e4d105..2fe0aabe5f 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp @@ -10,13 +10,13 @@ * */ -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h similarity index 100% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index aec3e7870f..829fe7e495 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -10,12 +10,22 @@ * */ -#include -#include +#include +#include +#include + +#include +#include +#include + +#include #include #include +#include #include #include +#include +#include namespace Multiplayer { @@ -23,8 +33,12 @@ namespace Multiplayer AZ_CVAR(bool, editorsv_enabled, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); + AZ_CVAR(bool, editorsv_launch, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "Whether Editor should launch a server when the server address is localhost"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); + AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(uint16_t, editorsv_port, DefaultServerEditorPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -57,12 +71,14 @@ namespace Multiplayer void MultiplayerEditorSystemComponent::Activate() { + AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } void MultiplayerEditorSystemComponent::Deactivate() { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + AzFramework::GameEntityContextEventBus::Handler::BusDisconnect(); } void MultiplayerEditorSystemComponent::NotifyRegisterViews() @@ -77,50 +93,6 @@ namespace Multiplayer { switch (event) { - case eNotify_OnBeginGameMode: - { - AZ::TickBus::Handler::BusConnect(); - - if (editorsv_enabled) - { - // Assemble the server's path - AZ::CVarFixedString serverProcess = editorsv_process; - if (serverProcess.empty()) - { - // If enabled but no process name is supplied, try this project's ServerLauncher - serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - } - - AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); - if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) - { - // If only the process name is specified, append that as well - serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); - } - else - { - // If any path was already specified, then simply assign - serverPath = serverProcess; - } - - if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) - { - // Add this platform's exe extension if it's not specified - serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); - } - - // Start the configured server if it's available - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = - AZStd::string::format("\"%s\"", serverPath.c_str()); - processLaunchInfo.m_showWindow = true; - processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; - - m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( - processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); - } - break; - } case eNotify_OnQuit: AZ_Warning("Multiplayer Editor", m_editor != nullptr, "Multiplayer Editor received On Quit without an Editor pointer."); if (m_editor) @@ -130,25 +102,132 @@ namespace Multiplayer } [[fallthrough]]; case eNotify_OnEndGameMode: - AZ::TickBus::Handler::BusDisconnect(); // Kill the configured server if it's active if (m_serverProcess) { m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); + if (editorNetworkInterface) + { + editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByClient); + } break; } } - void MultiplayerEditorSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + AzFramework::ProcessWatcher* LaunchEditorServer() { + // Assemble the server's path + AZ::CVarFixedString serverProcess = editorsv_process; + AZ::IO::FixedMaxPath serverPath; + if (serverProcess.empty()) + { + // If enabled but no process name is supplied, try this project's ServerLauncher + serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; + serverPath = AZ::Utils::GetExecutableDirectory(); + serverPath /= serverProcess + AZ_TRAIT_OS_EXECUTABLE_EXTENSION; + } + else + { + serverPath = serverProcess; + } + // Start the configured server if it's available + AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; + processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); + processLaunchInfo.m_showWindow = true; + processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; + + // Launch the Server and give it a few seconds to boot up + AzFramework::ProcessWatcher* outProcess = AzFramework::ProcessWatcher::LaunchProcess( + processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); + if (outProcess) + { + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + } + + return outProcess; } - int MultiplayerEditorSystemComponent::GetTickOrder() + void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() { - // Tick immediately after the network system component - return AZ::TICK_PLACEMENT + 1; + auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); + if (!prefabEditorEntityOwnershipInterface) + { + AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); + } + + // BeginGameMode and Prefab Processing have completed at this point + IMultiplayerTools* mpTools = AZ::Interface::Get(); + if (editorsv_enabled && mpTools != nullptr && mpTools->DidProcessNetworkPrefabs()) + { + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); + + AZStd::vector buffer; + AZ::IO::ByteContainerStream byteStream(&buffer); + + // Serialize Asset information and AssetData into a potentially large buffer + for (const auto& asset : assetData) + { + AZ::Data::AssetId assetId = asset.GetId(); + AZStd::string assetHint = asset.GetHint(); + uint32_t hintSize = aznumeric_cast(assetHint.size()); + + byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + byteStream.Write(sizeof(uint32_t), reinterpret_cast(&hintSize)); + byteStream.Write(assetHint.size(), assetHint.data()); + AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); + } + + const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; + if (editorsv_launch && LocalHost == remoteAddress) + { + m_serverProcess = LaunchEditorServer(); + } + + // Now that the server has launched, attempt to connect the NetworkInterface + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); + AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect."); + m_editorConnId = editorNetworkInterface->Connect( + AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); + + if (m_editorConnId == AzNetworking::InvalidConnectionId) + { + AZ_Warning( + "MultiplayerEditor", false, + "Could not connect to server targeted by Editor. If using a local server, check that it's built and editorsv_launch is true."); + return; + } + + // Read the buffer into EditorServerInit packets until we've flushed the whole thing + byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + + while (byteStream.GetCurPos() < byteStream.GetLength()) + { + MultiplayerEditorPackets::EditorServerInit packet; + AzNetworking::TcpPacketEncodingBuffer& outBuffer = packet.ModifyAssetData(); + + // Size the packet's buffer appropriately + size_t readSize = TcpPacketEncodingBuffer::GetCapacity(); + size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); + if (byteStreamSize < readSize) + { + readSize = byteStreamSize; + } + + outBuffer.Resize(readSize); + byteStream.Read(readSize, outBuffer.GetBuffer()); + + // If we've run out of buffer, mark that we're done + if (byteStream.GetCurPos() == byteStream.GetLength()) + { + packet.SetLastUpdate(true); + } + editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); + } + } + } } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 8c18a2e57a..81b138c675 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -14,15 +14,16 @@ #include +#include + #include #include #include #include - +#include #include #include - namespace AzNetworking { class INetworkInterface; @@ -33,7 +34,7 @@ namespace Multiplayer //! Multiplayer system component wraps the bridging logic between the game and transport layer. class MultiplayerEditorSystemComponent final : public AZ::Component - , private AZ::TickBus::Handler + , private AzFramework::GameEntityContextEventBus::Handler , private AzToolsFramework::EditorEvents::Bus::Handler , private IEditorNotifyListener { @@ -59,17 +60,19 @@ namespace Multiplayer void NotifyRegisterViews() override; //! @} - private: - - //! AZ::TickBus::Handler overrides. + private: + //! EditorEvents::Handler overrides //! @{ - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; - int GetTickOrder() override; - //! @} - //! void OnEditorNotifyEvent(EEditorNotifyEvent event) override; + //! @} + + //! GameEntityContextEventBus::Handler overrides + //! @{ + void OnGameEntitiesStarted() override; + //! @} IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; + AzNetworking::ConnectionId m_editorConnId; }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 8eb5bf7b0c..aa6fe6e72c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -10,22 +10,27 @@ * */ -#include -#include -#include -#include -#include -#include -#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include + #include +#include #include #include #include #include #include +#include #include +#include namespace AZ::ConsoleTypeHelpers { @@ -59,11 +64,8 @@ namespace Multiplayer { using namespace AzNetworking; - static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); - static constexpr uint16_t DefaultServerPort = 30090; - AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); - AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); + AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); AZ_CVAR(AZ::CVarFixedString, cl_serverpassword, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Optional server password"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); @@ -140,7 +142,7 @@ namespace Multiplayer void MultiplayerSystemComponent::Activate() { AZ::TickBus::Handler::BusConnect(); - m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(s_networkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); + m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(MPNetworkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); m_consoleCommandHandler.Connect(AZ::Interface::Get()->GetConsoleCommandInvokedEvent()); AZ::Interface::Register(this); @@ -664,7 +666,7 @@ namespace Multiplayer { Multiplayer::MultiplayerAgentType serverType = sv_isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer; AZ::Interface::Get()->InitializeMultiplayer(serverType); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); networkInterface->Listen(sv_port); } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); @@ -672,7 +674,7 @@ namespace Multiplayer void connect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); if (arguments.size() < 1) { @@ -702,7 +704,7 @@ namespace Multiplayer void disconnect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Uninitialized); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; networkInterface->GetConnectionSet().VisitConnections(visitor); } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 6bedd0599b..1410a82a3c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -12,17 +12,20 @@ #pragma once +#include +#include +#include +#include +#include + #include #include #include #include +#include #include #include #include -#include -#include -#include -#include namespace AzNetworking { @@ -72,7 +75,7 @@ namespace Multiplayer bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::NotifyClientMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EntityMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::ReadyForEntityUpdates& packet); - + //! IConnectionListener interface //! @{ AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; @@ -109,6 +112,7 @@ namespace Multiplayer AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; AZ::ConsoleCommandInvokedEvent::Handler m_consoleCommandHandler; AZ::ThreadSafeDeque m_cvarCommands; @@ -124,5 +128,9 @@ namespace Multiplayer AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId; + +#if !defined(AZ_RELEASE_BUILD) + MultiplayerEditorConnection m_editorConnectionListener; +#endif }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp index 5a223d6214..3646dd52a4 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp @@ -10,40 +10,40 @@ * */ -#include -#include +#include +#include #include + #include #include namespace Multiplayer { - //! Multiplayer Tools system component provides serialize context reflection for tools-only systems. - class MultiplayerToolsSystemComponent final - : public AZ::Component + + void MultiplayerToolsSystemComponent::Reflect(AZ::ReflectContext* context) { - public: - AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + NetworkPrefabProcessor::Reflect(context); + } - static void Reflect(AZ::ReflectContext* context) - { - NetworkPrefabProcessor::Reflect(context); - } + void MultiplayerToolsSystemComponent::Activate() + { + AZ::Interface::Register(this); + } - MultiplayerToolsSystemComponent() = default; - ~MultiplayerToolsSystemComponent() override = default; + void MultiplayerToolsSystemComponent::Deactivate() + { + AZ::Interface::Unregister(this); + } - /// AZ::Component overrides. - void Activate() override - { + bool MultiplayerToolsSystemComponent::DidProcessNetworkPrefabs() + { + return m_didProcessNetPrefabs; + } - } - - void Deactivate() override - { - - } - }; + void MultiplayerToolsSystemComponent::SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) + { + m_didProcessNetPrefabs = didProcessNetPrefabs; + } MultiplayerToolsModule::MultiplayerToolsModule() : AZ::Module() diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h index 823bd63a1d..b05e2aa5fb 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h @@ -12,10 +12,36 @@ #pragma once +#include #include +#include namespace Multiplayer { + class MultiplayerToolsSystemComponent final + : public AZ::Component + , public IMultiplayerTools + { + public: + AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + + static void Reflect(AZ::ReflectContext* context); + + MultiplayerToolsSystemComponent() = default; + ~MultiplayerToolsSystemComponent() override = default; + + /// AZ::Component overrides. + void Activate() override; + void Deactivate() override; + + bool DidProcessNetworkPrefabs() override; + + private: + void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) override; + + bool m_didProcessNetPrefabs = false; + }; + class MultiplayerToolsModule : public AZ::Module { diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index 93136ab261..0bbfe17801 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -10,7 +10,11 @@ * */ -#include +#include +#include +#include +#include +#include #include #include @@ -18,9 +22,6 @@ #include #include #include -#include -#include -#include namespace Multiplayer { @@ -29,9 +30,20 @@ namespace Multiplayer void NetworkPrefabProcessor::Process(PrefabProcessorContext& context) { + IMultiplayerTools* mpTools = AZ::Interface::Get(); + if (mpTools) + { + mpTools->SetDidProcessNetworkPrefabs(false); + } + context.ListPrefabs([&context](AZStd::string_view prefabName, PrefabDom& prefab) { ProcessPrefab(context, prefabName, prefab); }); + + if (mpTools && !context.GetProcessedObjects().empty()) + { + mpTools->SetDidProcessNetworkPrefabs(true); + } } void NetworkPrefabProcessor::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp index 2e0b9c0759..6ace4db592 100644 --- a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp +++ b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ namespace UnitTest SetupAllocator(); AZ::NameDictionary::Create(); m_spawnableComponent = new AzFramework::SpawnableSystemComponent(); + m_netComponent = new AzNetworking::NetworkingSystemComponent(); m_mpComponent = new Multiplayer::MultiplayerSystemComponent(); m_initHandler = Multiplayer::SessionInitEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestInitEvent(value); }); @@ -43,6 +45,7 @@ namespace UnitTest void TearDown() override { delete m_mpComponent; + delete m_netComponent; delete m_spawnableComponent; AZ::NameDictionary::Destroy(); TeardownAllocator(); @@ -71,6 +74,7 @@ namespace UnitTest Multiplayer::SessionShutdownEvent::Handler m_shutdownHandler; Multiplayer::ConnectionAcquiredEvent::Handler m_connAcquiredHandler; + AzNetworking::NetworkingSystemComponent* m_netComponent = nullptr; Multiplayer::MultiplayerSystemComponent* m_mpComponent = nullptr; AzFramework::SpawnableSystemComponent* m_spawnableComponent = nullptr; }; diff --git a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_files.cmake deleted file mode 100644 index ce3e3227e0..0000000000 --- a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# -# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -# its licensors. -# -# For complete copyright and license terms please see the LICENSE at the root of this -# distribution (the "License"). All use of this software is governed by the License, -# or, if provided, by the license below or the license accompanying this file. Do not -# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# - -set(FILES - Source/Editor/MultiplayerEditorDispatcher.cpp - Source/Editor/MultiplayerEditorDispatcher.h -) diff --git a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake index 2d5611d4b6..3fb76061b8 100644 --- a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake @@ -12,8 +12,8 @@ set(FILES Source/MultiplayerGem.cpp Source/MultiplayerGem.h - Source/MultiplayerEditorGem.cpp - Source/MultiplayerEditorGem.h + Source/Editor/MultiplayerEditorGem.cpp + Source/Editor/MultiplayerEditorGem.h Source/Editor/MultiplayerEditorSystemComponent.cpp Source/Editor/MultiplayerEditorSystemComponent.h ) diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index addd4ea810..eb856a48db 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -11,6 +11,8 @@ set(FILES Include/Multiplayer/IMultiplayer.h + Include/Multiplayer/IMultiplayerTools.h + Include/Multiplayer/MultiplayerConstants.h Include/Multiplayer/MultiplayerStats.h Include/Multiplayer/MultiplayerTypes.h Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -46,6 +48,7 @@ set(FILES Source/AutoGen/AutoComponentTypes_Source.jinja Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml Source/AutoGen/Multiplayer.AutoPackets.xml + Source/AutoGen/MultiplayerEditor.AutoPackets.xml Source/AutoGen/NetworkTransformComponent.AutoComponent.xml Source/Components/LocalPredictionPlayerInputComponent.cpp Source/Components/MultiplayerComponent.cpp @@ -59,6 +62,8 @@ set(FILES Source/ConnectionData/ServerToClientConnectionData.cpp Source/ConnectionData/ServerToClientConnectionData.h Source/ConnectionData/ServerToClientConnectionData.inl + Source/Editor/MultiplayerEditorConnection.cpp + Source/Editor/MultiplayerEditorConnection.h Source/EntityDomains/FullOwnershipEntityDomain.cpp Source/EntityDomains/FullOwnershipEntityDomain.h Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp diff --git a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake index 1be02fd999..3fef954ba6 100644 --- a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake @@ -10,6 +10,7 @@ # set(FILES + Include/Multiplayer/IMultiplayerTools.h Source/Multiplayer_precompiled.cpp Source/Multiplayer_precompiled.h Source/Pipeline/NetworkPrefabProcessor.cpp diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp index 473e9534cb..6fa69c7a24 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.cpp @@ -73,7 +73,10 @@ namespace PhysX { } - CharacterControllerComponent::~CharacterControllerComponent() = default; + CharacterControllerComponent::~CharacterControllerComponent() + { + DisableController(); + } // AZ::Component void CharacterControllerComponent::Init() @@ -92,7 +95,7 @@ namespace PhysX void CharacterControllerComponent::Deactivate() { - DestroyController(); + DisableController(); Physics::CollisionFilteringRequestBus::Handler::BusDisconnect(); AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); @@ -198,7 +201,7 @@ namespace PhysX void CharacterControllerComponent::DisablePhysics() { - DestroyController(); + DisableController(); } bool CharacterControllerComponent::IsPhysicsEnabled() const @@ -421,17 +424,32 @@ namespace PhysX AZ::TransformBus::EventResult(entityTranslation, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation); m_characterConfig->m_position = entityTranslation; - if (auto* sceneInterface = AZ::Interface::Get()) + auto* sceneInterface = AZ::Interface::Get(); + if (sceneInterface != nullptr) { - AzPhysics::SimulatedBodyHandle bodyHandle = sceneInterface->AddSimulatedBody(defaultSceneHandle, m_characterConfig.get()); - m_controller = azdynamic_cast(sceneInterface->GetSimulatedBodyFromHandle(defaultSceneHandle, bodyHandle)); + m_controllerBodyHandle = sceneInterface->AddSimulatedBody(defaultSceneHandle, m_characterConfig.get()); + m_controller = azdynamic_cast( + sceneInterface->GetSimulatedBodyFromHandle(defaultSceneHandle, m_controllerBodyHandle)); } if (m_controller == nullptr) { AZ_Error("PhysX Character Controller Component", false, "Failed to create character controller."); return; } - + + if (sceneInterface != nullptr) + { + // if the scene removes this controller body, we should also clean up our resources. + m_onSimulatedBodyRemovedHandler = AzPhysics::SceneEvents::OnSimulationBodyRemoved::Handler( + [this]([[maybe_unused]] AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle) { + if (bodyHandle == m_controllerBodyHandle) + { + DestroyController(); + } + }); + sceneInterface->RegisterSimulationBodyRemovedHandler(defaultSceneHandle, m_onSimulatedBodyRemovedHandler); + } + CharacterControllerRequestBus::Handler::BusConnect(GetEntityId()); m_preSimulateHandler = AzPhysics::SystemEvents::OnPresimulateEvent::Handler( @@ -447,7 +465,7 @@ namespace PhysX } } - void CharacterControllerComponent::DestroyController() + void CharacterControllerComponent::DisableController() { if (!IsPhysicsEnabled()) { @@ -460,10 +478,15 @@ namespace PhysX { sceneInterface->RemoveSimulatedBody(m_controller->m_sceneOwner, m_controller->m_bodyHandle); } + + DestroyController(); + } + + void CharacterControllerComponent::DestroyController() + { m_controller = nullptr; - m_preSimulateHandler.Disconnect(); - + m_onSimulatedBodyRemovedHandler.Disconnect(); CharacterControllerRequestBus::Handler::BusDisconnect(); } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h index a7a1a92ad2..7c25312b72 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h +++ b/Gems/PhysX/Code/Source/PhysXCharacters/Components/CharacterControllerComponent.h @@ -131,7 +131,12 @@ namespace PhysX void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled) override; private: + // Creates the physics character controller in the current default physics scene. + // This will do nothing if the controller is already created. void CreateController(); + // Removes the physics character controller from the scene and will call DestroyController for clean up. + void DisableController(); + // Cleans up all references and events used with the physics character controller. void DestroyController(); void OnPreSimulate(float deltaTime); @@ -139,6 +144,8 @@ namespace PhysX AZStd::unique_ptr m_characterConfig; AZStd::shared_ptr m_shapeConfig; PhysX::CharacterController* m_controller = nullptr; + AzPhysics::SimulatedBodyHandle m_controllerBodyHandle = AzPhysics::InvalidSimulatedBodyHandle; AzPhysics::SystemEvents::OnPresimulateEvent::Handler m_preSimulateHandler; + AzPhysics::SceneEvents::OnSimulationBodyRemoved::Handler m_onSimulatedBodyRemovedHandler; }; } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp index 79aa767959..689ea47be7 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp +++ b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp @@ -489,6 +489,7 @@ namespace PhysX // Disable simulation on body (not signaling OnSimulationBodySimulationDisabled event) DisableSimulationOfBodyInternal(*simulatedBody.second); } + m_simulatedBodyRemovedEvent.Signal(m_sceneHandle, simulatedBody.second->m_bodyHandle); delete simulatedBody.second; } } diff --git a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp index 592a8e2a75..45908eb2d5 100644 --- a/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp +++ b/Gems/SceneProcessing/Code/Source/Generation/Components/MeshOptimizer/MeshOptimizerComponent.cpp @@ -135,7 +135,7 @@ namespace AZ::SceneGenerationComponents { for (size_t controlPointIndex = 0; controlPointIndex < skinData.get().GetVertexCount(); ++controlPointIndex) { - const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(aznumeric_caster(controlPointIndex)); + const int usedPointIndex = meshData->GetUsedPointIndexForControlPoint(meshData->GetControlPointIndex(aznumeric_caster(controlPointIndex))); const size_t linkCount = skinData.get().GetLinkCount(controlPointIndex); if (usedPointIndex < 0 || linkCount == 0) diff --git a/cmake/Packaging.cmake b/cmake/Packaging.cmake index 4f6565edc7..ba610b1883 100644 --- a/cmake/Packaging.cmake +++ b/cmake/Packaging.cmake @@ -13,7 +13,34 @@ if(NOT PAL_TRAIT_BUILD_CPACK_SUPPORTED) return() endif() -ly_get_absolute_pal_filename(pal_dir ${CMAKE_SOURCE_DIR}/cmake/Platform/${PAL_HOST_PLATFORM_NAME}) +# public facing options will be used for conversion into cpack specific ones below. +set(LY_INSTALLER_DOWNLOAD_URL "" CACHE STRING "URL embedded into the installer to download additional artifacts") +set(LY_INSTALLER_LICENSE_URL "" CACHE STRING "Optionally embed a link to the license instead of raw text") + +# set all common cpack variable overrides first so they can be accessible via configure_file +# when the platform specific settings are applied below. additionally, any variable with +# the "CPACK_" prefix will automatically be cached for use in any phase of cpack namely +# pre/post build +set(CPACK_PACKAGE_VENDOR "${PROJECT_NAME}") +set(CPACK_PACKAGE_VERSION "${LY_VERSION_STRING}") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Installation Tool") + +string(TOLOWER ${PROJECT_NAME} _project_name_lower) +set(CPACK_PACKAGE_FILE_NAME "${_project_name_lower}_${LY_VERSION_STRING}_installer") + +set(DEFAULT_LICENSE_NAME "Apache-2.0") +set(DEFAULT_LICENSE_FILE "${CMAKE_SOURCE_DIR}/LICENSE.txt") + +set(CPACK_RESOURCE_FILE_LICENSE ${DEFAULT_LICENSE_FILE}) +set(CPACK_LICENSE_URL ${LY_INSTALLER_LICENSE_URL}) + +set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_VENDOR}/${CPACK_PACKAGE_VERSION}") + +# CMAKE_SOURCE_DIR doesn't equate to anything during execution of pre/post build scripts +set(CPACK_SOURCE_DIR ${CMAKE_SOURCE_DIR}/cmake) + +# attempt to apply platform specific settings +ly_get_absolute_pal_filename(pal_dir ${CPACK_SOURCE_DIR}/Platform/${PAL_HOST_PLATFORM_NAME}) include(${pal_dir}/Packaging_${PAL_HOST_PLATFORM_NAME_LOWERCASE}.cmake) # if we get here and the generator hasn't been set, then a non fatal error occurred disabling packaging support @@ -21,20 +48,6 @@ if(NOT CPACK_GENERATOR) return() endif() -set(CPACK_PACKAGE_VENDOR "${PROJECT_NAME}") -set(CPACK_PACKAGE_VERSION "${LY_VERSION_STRING}") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Installation Tool") - -string(TOLOWER ${PROJECT_NAME} _project_name_lower) -set(CPACK_PACKAGE_FILE_NAME "${_project_name_lower}_installer") - -set(DEFAULT_LICENSE_NAME "Apache-2.0") -set(DEFAULT_LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt") - -set(CPACK_RESOURCE_FILE_LICENSE ${DEFAULT_LICENSE_FILE}) - -set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_VENDOR}/${CPACK_PACKAGE_VERSION}") - # IMPORTANT: required to be included AFTER setting all property overrides include(CPack REQUIRED) @@ -76,3 +89,12 @@ ly_configure_cpack_component( DISPLAY_NAME "${PROJECT_NAME} Core" DESCRIPTION "${PROJECT_NAME} Headers, Libraries and Tools" ) + +if(LY_INSTALLER_DOWNLOAD_URL) + # this will set the following variables: CPACK_DOWNLOAD_SITE, CPACK_DOWNLOAD_ALL, and CPACK_UPLOAD_DIRECTORY + cpack_configure_downloads( + ${LY_INSTALLER_DOWNLOAD_URL} + UPLOAD_DIRECTORY ${CMAKE_BINARY_DIR}/_CPack_Uploads # to match the _CPack_Packages directory + ALL + ) +endif() diff --git a/cmake/Platform/Windows/PackagingBootstrapper.wxs b/cmake/Platform/Windows/PackagingBootstrapper.wxs new file mode 100644 index 0000000000..c3d1dd7a7b --- /dev/null +++ b/cmake/Platform/Windows/PackagingBootstrapper.wxs @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmake/Platform/Windows/PackagingPostBuild.cmake b/cmake/Platform/Windows/PackagingPostBuild.cmake new file mode 100644 index 0000000000..3ba6ef2096 --- /dev/null +++ b/cmake/Platform/Windows/PackagingPostBuild.cmake @@ -0,0 +1,88 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +# convert the path to a windows style path using string replace because TO_NATIVE_PATH +# only works on real paths +string(REPLACE "/" "\\" _fixed_package_install_dir ${CPACK_PACKAGE_INSTALL_DIRECTORY}) + +# directory where the auto generated files live e.g /_CPack_Package/win64/WIX +set(_cpack_wix_out_dir ${CPACK_TOPLEVEL_DIRECTORY}) +set(_bootstrap_out_dir "${CPACK_TOPLEVEL_DIRECTORY}/bootstrap") + +set(_bootstrap_filename "${CPACK_PACKAGE_FILE_NAME}.exe") +set(_bootstrap_output_file ${_cpack_wix_out_dir}/${_bootstrap_filename}) + +set(_ext_flags + -ext WixBalExtension +) + +set(_addtional_defines + -dCPACK_BOOTSTRAP_UPGRADE_GUID=${CPACK_WIX_BOOTSTRAP_UPGRADE_GUID} + -dCPACK_DOWNLOAD_SITE=${CPACK_DOWNLOAD_SITE} + -dCPACK_LOCAL_INSTALLER_DIR=${_cpack_wix_out_dir} + -dCPACK_PACKAGE_FILE_NAME=${CPACK_PACKAGE_FILE_NAME} + -dCPACK_PACKAGE_INSTALL_DIRECTORY=${_fixed_package_install_dir} +) + +if(CPACK_LICENSE_URL) + list(APPEND _addtional_defines -dCPACK_LICENSE_URL=${CPACK_LICENSE_URL}) +endif() + +set(_candle_command + ${CPACK_WIX_CANDLE_EXECUTABLE} + -nologo + -arch x64 + "-I${_cpack_wix_out_dir}" # to include cpack_variables.wxi + ${_addtional_defines} + ${_ext_flags} + "${CPACK_SOURCE_DIR}/Platform/Windows/PackagingBootstrapper.wxs" + -o "${_bootstrap_out_dir}/" +) + +set(_light_command + ${CPACK_WIX_LIGHT_EXECUTABLE} + -nologo + ${_ext_flags} + ${_bootstrap_out_dir}/*.wixobj + -o "${_bootstrap_output_file}" +) + +message(STATUS "Creating Bootstrap Installer...") +execute_process( + COMMAND ${_candle_command} + COMMAND_ERROR_IS_FATAL ANY +) +execute_process( + COMMAND ${_light_command} + COMMAND_ERROR_IS_FATAL ANY +) + +file(COPY ${_bootstrap_output_file} + DESTINATION ${CPACK_PACKAGE_DIRECTORY} +) + +message(STATUS "Bootstrap installer generated to ${CPACK_PACKAGE_DIRECTORY}/${_bootstrap_filename}") + +# use the internal default path if somehow not specified from cpack_configure_downloads +if(NOT CPACK_UPLOAD_DIRECTORY) + set(CPACK_UPLOAD_DIRECTORY ${CPACK_PACKAGE_DIRECTORY}/CPackUploads) +endif() + +# copy the artifacts intended to be uploaded to a remote server into the folder specified +# through cpack_configure_downloads. this mimics the same process cpack does natively for +# some other frameworks that have built-in online installer support. +message(STATUS "Copying installer artifacts to upload directory...") +file(REMOVE_RECURSE ${CPACK_UPLOAD_DIRECTORY}) +file(GLOB _artifacts "${_cpack_wix_out_dir}/*.msi" "${_cpack_wix_out_dir}/*.cab") +file(COPY ${_artifacts} + DESTINATION ${CPACK_UPLOAD_DIRECTORY} +) +message(STATUS "Artifacts copied to ${CPACK_UPLOAD_DIRECTORY}") diff --git a/cmake/Platform/Windows/PackagingTemplate.wxs.in b/cmake/Platform/Windows/PackagingTemplate.wxs.in index 3e5db03ec2..0b3c597ab6 100644 --- a/cmake/Platform/Windows/PackagingTemplate.wxs.in +++ b/cmake/Platform/Windows/PackagingTemplate.wxs.in @@ -12,10 +12,10 @@ Manufacturer="$(var.CPACK_PACKAGE_VENDOR)" UpgradeCode="$(var.CPACK_WIX_UPGRADE_GUID)"> - + - - + +