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/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/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/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;
-
- // 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;
-
- ApplyIblSpecular(
- surface.position,
- surface.clearCoat.normal,
- clearCoatSpecularF0,
- surface.clearCoat.roughness,
- lightingData.dirToCamera,
- clearCoatBrdf,
- clearCoatIblSpecular);
-
- clearCoatIblSpecular *= surface.clearCoat.factor;
+ 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 = GetIblSpecular(surface.position, surface.clearCoat.normal, clearCoatSpecularF0, surface.clearCoat.roughness, lightingData.dirToCamera, clearCoatBrdf);
+
+ 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/MorphTargets/MorphTargetComputePass.cpp b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.cpp
index 35793b82a3..1bb537ecef 100644
--- a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.cpp
@@ -12,6 +12,7 @@
#include
+#include
#include
#include
@@ -38,6 +39,11 @@ namespace AZ
return m_shader;
}
+ void MorphTargetComputePass::SetFeatureProcessor(SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor)
+ {
+ m_skinnedMeshFeatureProcessor = skinnedMeshFeatureProcessor;
+ }
+
void MorphTargetComputePass::BuildAttachmentsInternal()
{
// The same buffer that skinning writes to is used to manage the computed vertex deltas that are passed from the
@@ -45,30 +51,16 @@ namespace AZ
AttachBufferToSlot(Name{ "MorphTargetDeltaOutput" }, SkinnedMeshOutputStreamManagerInterface::Get()->GetBuffer());
}
- void MorphTargetComputePass::AddDispatchItem(const RHI::DispatchItem* dispatchItem)
- {
- AZ_Assert(dispatchItem != nullptr, "invalid dispatchItem");
-
- AZStd::lock_guard lock(m_mutex);
- //using an unordered_set here to prevent redundantly adding the same dispatchItem to the submission queue
- //(i.e. if the same morph target exists in multiple views, it can call AddDispatchItem multiple times with the same item)
- m_dispatches.insert(dispatchItem);
- }
-
void MorphTargetComputePass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
{
- RHI::CommandList* commandList = context.GetCommandList();
+ if (m_skinnedMeshFeatureProcessor)
+ {
+ RHI::CommandList* commandList = context.GetCommandList();
- SetSrgsForDispatch(commandList);
+ SetSrgsForDispatch(commandList);
- AZStd::lock_guard lock(m_mutex);
- for (const RHI::DispatchItem* dispatchItem : m_dispatches)
- {
- commandList->Submit(*dispatchItem);
+ m_skinnedMeshFeatureProcessor->SubmitMorphTargetDispatchItems(commandList);
}
-
- // Clear the dispatch items. They will need to be re-populated next frame
- m_dispatches.clear();
}
} // namespace Render
} // namespace AZ
diff --git a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.h b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.h
index 3967d9190e..61fa485fbc 100644
--- a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.h
+++ b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetComputePass.h
@@ -18,6 +18,8 @@ namespace AZ
{
namespace Render
{
+ class SkinnedMeshFeatureProcessor;
+
//! The morph target compute pass submits dispatch items for morph targets. The dispatch items are cleared every frame, so it needs to be re-populated.
class MorphTargetComputePass
: public RPI::ComputePass
@@ -31,16 +33,14 @@ namespace AZ
static RPI::Ptr Create(const RPI::PassDescriptor& descriptor);
- //! Thread-safe function for adding a dispatch item to the current frame.
- void AddDispatchItem(const RHI::DispatchItem* dispatchItem);
Data::Instance GetShader() const;
+ void SetFeatureProcessor(SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor);
private:
void BuildAttachmentsInternal() override;
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
- AZStd::mutex m_mutex;
- AZStd::unordered_set m_dispatches;
+ SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor = nullptr;
};
}
}
diff --git a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.cpp b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.cpp
index d7bbc315ed..7b3aedd64e 100644
--- a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.cpp
@@ -11,7 +11,7 @@
*/
#include
-#include
+#include
#include
#include
@@ -30,7 +30,7 @@ namespace AZ
MorphTargetDispatchItem::MorphTargetDispatchItem(
const AZStd::intrusive_ptr inputBuffers,
const MorphTargetMetaData& morphTargetMetaData,
- RPI::Ptr morphTargetComputePass,
+ SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
MorphTargetInstanceMetaData morphInstanceMetaData,
float morphDeltaIntegerEncoding)
: m_inputBuffers(inputBuffers)
@@ -38,7 +38,7 @@ namespace AZ
, m_morphInstanceMetaData(morphInstanceMetaData)
, m_accumulatedDeltaIntegerEncoding(morphDeltaIntegerEncoding)
{
- m_morphTargetShader = morphTargetComputePass->GetShader();
+ m_morphTargetShader = skinnedMeshFeatureProcessor->GetMorphTargetShader();
RPI::ShaderReloadNotificationBus::Handler::BusConnect(m_morphTargetShader->GetAssetId());
}
diff --git a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.h b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.h
index 46680eb465..ad1fd969a5 100644
--- a/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.h
+++ b/Gems/Atom/Feature/Common/Code/Source/MorphTargets/MorphTargetDispatchItem.h
@@ -37,7 +37,7 @@ namespace AZ
namespace Render
{
- class MorphTargetComputePass;
+ class SkinnedMeshFeatureProcessor;
//! Holds and manages an RHI DispatchItem for a specific morph target, and the resources that are needed to build and maintain it.
class MorphTargetDispatchItem
@@ -51,7 +51,7 @@ namespace AZ
explicit MorphTargetDispatchItem(
const AZStd::intrusive_ptr inputBuffers,
const MorphTargetMetaData& morphTargetMetaData,
- RPI::Ptr morphTargetComputePass,
+ SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
MorphTargetInstanceMetaData morphInstanceMetaData,
float accumulatedDeltaRange
);
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.cpp
index d7b2c605b4..a3feddb0b6 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.cpp
@@ -12,6 +12,7 @@
#include
+#include
#include
#include
@@ -22,11 +23,9 @@ namespace AZ
{
namespace Render
{
-
SkinnedMeshComputePass::SkinnedMeshComputePass(const RPI::PassDescriptor& descriptor)
: RPI::ComputePass(descriptor)
{
- m_cachedShaderOptions.SetShader(m_shader);
}
RPI::Ptr SkinnedMeshComputePass::Create(const RPI::PassDescriptor& descriptor)
@@ -40,42 +39,30 @@ namespace AZ
return m_shader;
}
- RPI::ShaderOptionGroup SkinnedMeshComputePass::CreateShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler)
+ void SkinnedMeshComputePass::SetFeatureProcessor(SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor)
{
- m_cachedShaderOptions.ConnectToShaderReinitializedEvent(shaderReinitializedHandler);
- return m_cachedShaderOptions.CreateShaderOptionGroup(shaderOptions);
- }
-
- void SkinnedMeshComputePass::AddDispatchItem(const RHI::DispatchItem* dispatchItem)
- {
- AZ_Assert(dispatchItem != nullptr, "invalid dispatchItem");
-
- AZStd::lock_guard lock(m_mutex);
- //using an unordered_set here to prevent redundantly adding the same dispatchItem to the submission queue
- //(i.e. if the same skinnedMesh exists in multiple views, it can call AddDispatchItem multiple times with the same item)
- m_dispatches.insert(dispatchItem);
+ m_skinnedMeshFeatureProcessor = skinnedMeshFeatureProcessor;
}
void SkinnedMeshComputePass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
{
- RHI::CommandList* commandList = context.GetCommandList();
+ if (m_skinnedMeshFeatureProcessor)
+ {
+ RHI::CommandList* commandList = context.GetCommandList();
- SetSrgsForDispatch(commandList);
+ SetSrgsForDispatch(commandList);
- AZStd::lock_guard lock(m_mutex);
- for (const RHI::DispatchItem* dispatchItem : m_dispatches)
- {
- commandList->Submit(*dispatchItem);
+ m_skinnedMeshFeatureProcessor->SubmitSkinningDispatchItems(commandList);
}
-
- // Clear the dispatch items. They will need to be re-populated next frame
- m_dispatches.clear();
}
void SkinnedMeshComputePass::OnShaderReinitialized(const RPI::Shader& shader)
{
ComputePass::OnShaderReinitialized(shader);
- m_cachedShaderOptions.SetShader(m_shader);
+ if (m_skinnedMeshFeatureProcessor)
+ {
+ m_skinnedMeshFeatureProcessor->OnSkinningShaderReinitialized(m_shader);
+ }
}
void SkinnedMeshComputePass::OnShaderVariantReinitialized(const RPI::Shader& shader, const RPI::ShaderVariantId&, RPI::ShaderVariantStableId)
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.h b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.h
index d2e0fb77dc..5f7ff08e47 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.h
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshComputePass.h
@@ -20,6 +20,8 @@ namespace AZ
{
namespace Render
{
+ class SkinnedMeshFeatureProcessor;
+
//! The skinned mesh compute pass submits dispatch items for skinning. The dispatch items are cleared every frame, so it needs to be re-populated.
class SkinnedMeshComputePass
: public RPI::ComputePass
@@ -33,10 +35,9 @@ namespace AZ
static RPI::Ptr Create(const RPI::PassDescriptor& descriptor);
- //! Thread-safe function for adding a dispatch item to the current frame.
- void AddDispatchItem(const RHI::DispatchItem* dispatchItem);
Data::Instance GetShader() const;
- RPI::ShaderOptionGroup CreateShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler);
+
+ void SetFeatureProcessor(SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor);
private:
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
@@ -45,9 +46,7 @@ namespace AZ
void OnShaderReinitialized(const RPI::Shader& shader) override;
void OnShaderVariantReinitialized(const RPI::Shader& shader, const RPI::ShaderVariantId& shaderVariantId, RPI::ShaderVariantStableId shaderVariantStableId) override;
- AZStd::mutex m_mutex;
- AZStd::unordered_set m_dispatches;
- CachedSkinnedMeshShaderOptions m_cachedShaderOptions;
+ SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor = nullptr;
};
}
}
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.cpp
index c9054fc6c8..647a87a788 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.cpp
@@ -12,7 +12,7 @@
#include
#include
-#include
+#include
#include
#include
@@ -34,7 +34,7 @@ namespace AZ
size_t lodIndex,
Data::Instance boneTransforms,
const SkinnedMeshShaderOptions& shaderOptions,
- RPI::Ptr skinnedMeshComputePass,
+ SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
MorphTargetInstanceMetaData morphTargetInstanceMetaData,
float morphTargetDeltaIntegerEncoding)
: m_inputBuffers(inputBuffers)
@@ -45,7 +45,7 @@ namespace AZ
, m_morphTargetInstanceMetaData(morphTargetInstanceMetaData)
, m_morphTargetDeltaIntegerEncoding(morphTargetDeltaIntegerEncoding)
{
- m_skinningShader = skinnedMeshComputePass->GetShader();
+ m_skinningShader = skinnedMeshFeatureProcessor->GetSkinningShader();
// Shader options are generally set per-skinned mesh instance, but morph targets may only exist on some lods. Override the option for applying morph targets here
if (m_morphTargetInstanceMetaData.m_accumulatedPositionDeltaOffsetInBytes != MorphTargetConstants::s_invalidDeltaOffset)
@@ -58,7 +58,7 @@ namespace AZ
}
// CreateShaderOptionGroup will also connect to the SkinnedMeshShaderOptionNotificationBus
- m_shaderOptionGroup = skinnedMeshComputePass->CreateShaderOptionGroup(m_shaderOptions, *this);
+ m_shaderOptionGroup = skinnedMeshFeatureProcessor->CreateSkinningShaderOptionGroup(m_shaderOptions, *this);
}
SkinnedMeshDispatchItem::~SkinnedMeshDispatchItem()
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.h b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.h
index c80b002106..33bec89afd 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.h
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshDispatchItem.h
@@ -38,7 +38,7 @@ namespace AZ
namespace Render
{
- class SkinnedMeshComputePass;
+ class SkinnedMeshFeatureProcessor;
//! Holds and manages an RHI DispatchItem for a specific skinned mesh, and the resources that are needed to build and maintain it.
class SkinnedMeshDispatchItem
@@ -55,7 +55,7 @@ namespace AZ
size_t lodIndex,
Data::Instance skinningMatrices,
const SkinnedMeshShaderOptions& shaderOptions,
- RPI::Ptr skinnedMeshComputePass,
+ SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
MorphTargetInstanceMetaData morphTargetInstanceMetaData,
float morphTargetDeltaIntegerEncoding
);
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
index 193a36e588..cb2d6a69ef 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp
@@ -24,8 +24,10 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
@@ -69,26 +71,11 @@ 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);
AZ_ATOM_PROFILE_FUNCTION("SkinnedMesh", "SkinnedMeshFeatureProcessor: Render");
- if (!m_skinningPass)
- {
- return;
- }
-
#if 0 //[GFX_TODO][ATOM-13564] Temporarily disable skinning culling until we figure out how to hook up visibility & lod selection with skinning:
//Setup the culling workgroup (it will be re-used for each view)
{
@@ -132,7 +119,7 @@ namespace AZ
//Dispatch the workgroup to each view
for (const RPI::ViewPtr& viewPtr : packet.m_views)
{
- Job *processWorkgroupJob = AZ::CreateJobFunction(
+ Job* processWorkgroupJob = AZ::CreateJobFunction(
[this, cullingSystem, viewPtr](AZ::Job& thisJob)
{
AZ_PROFILE_SCOPE_DYNAMIC(Debug::ProfileCategory::AzRender, "skinningMeshFP processWorkgroupJob - View: %s", viewPtr->GetName().GetCStr());
@@ -167,7 +154,16 @@ namespace AZ
float maxScreenPercentage(lod.m_range.m_max);
if (approxScreenPercentage >= minScreenPercentage && approxScreenPercentage <= maxScreenPercentage)
{
- m_skinningPass->AddDispatchItem(&renderProxy->m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
+ AZStd::lock_guard lock(m_dispatchItemMutex);
+ m_skinningDispatches.insert(&renderProxy->m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
+ for (size_t morphTargetIndex = 0; morphTargetIndex < renderProxy->m_morphTargetDispatchItemsByLod[lodIndex].size(); morphTargetIndex++)
+ {
+ const MorphTargetDispatchItem* dispatchItem = renderProxy->m_morphTargetDispatchItemsByLod[lodIndex][morphTargetIndex].get();
+ if (dispatchItem && dispatchItem->GetWeight() > AZ::Constants::FloatEpsilon)
+ {
+ m_morphTargetDispatches.insert(&dispatchItem->GetRHIDispatchItem());
+ }
+ }
}
}
}
@@ -232,13 +228,14 @@ namespace AZ
//Note that this supports overlapping lod ranges (to support cross-fading lods, for example)
if (approxScreenPercentage >= lod.m_screenCoverageMin && approxScreenPercentage <= lod.m_screenCoverageMax)
{
- m_skinningPass->AddDispatchItem(&renderProxy.m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
+ AZStd::lock_guard lock(m_dispatchItemMutex);
+ m_skinningDispatches.insert(&renderProxy.m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
for (size_t morphTargetIndex = 0; morphTargetIndex < renderProxy.m_morphTargetDispatchItemsByLod[lodIndex].size(); morphTargetIndex++)
{
const MorphTargetDispatchItem* dispatchItem = renderProxy.m_morphTargetDispatchItemsByLod[lodIndex][morphTargetIndex].get();
if (dispatchItem && dispatchItem->GetWeight() > AZ::Constants::FloatEpsilon)
{
- m_morphTargetPass->AddDispatchItem(&dispatchItem->GetRHIDispatchItem());
+ m_morphTargetDispatches.insert(&dispatchItem->GetRHIDispatchItem());
}
}
}
@@ -248,29 +245,32 @@ namespace AZ
#endif
}
- void SkinnedMeshFeatureProcessor::OnRenderPipelineAdded([[maybe_unused]] RPI::RenderPipelinePtr pipeline)
+ void SkinnedMeshFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline)
{
- InitSkinningAndMorphPass();
+ InitSkinningAndMorphPass(pipeline->GetRootPass());
}
- void SkinnedMeshFeatureProcessor::OnRenderPipelineRemoved([[maybe_unused]] RPI::RenderPipeline* pipeline)
+ void SkinnedMeshFeatureProcessor::OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline)
{
- InitSkinningAndMorphPass();
- }
-
- void SkinnedMeshFeatureProcessor::OnRenderPipelinePassesChanged([[maybe_unused]] RPI::RenderPipeline* renderPipeline)
- {
- InitSkinningAndMorphPass();
+ InitSkinningAndMorphPass(renderPipeline->GetRootPass());
}
void SkinnedMeshFeatureProcessor::OnBeginPrepareRender()
{
m_renderProxiesChecker.soft_lock();
+
+ SkinnedMeshFeatureProcessorNotificationBus::Broadcast(&SkinnedMeshFeatureProcessorNotificationBus::Events::OnUpdateSkinningMatrices);
}
- void SkinnedMeshFeatureProcessor::OnEndPrepareRender()
+ void SkinnedMeshFeatureProcessor::OnRenderEnd()
{
m_renderProxiesChecker.soft_unlock();
+
+ // Clear any dispatch items that were added but never submitted
+ // in case there were no passes that submitted this frame
+ // because they execute at a lower frequency
+ m_skinningDispatches.clear();
+ m_morphTargetDispatches.clear();
}
SkinnedMeshRenderProxyHandle SkinnedMeshFeatureProcessor::AcquireRenderProxy(const SkinnedMeshRenderProxyDesc& desc)
@@ -295,61 +295,73 @@ namespace AZ
return false;
}
- void SkinnedMeshFeatureProcessor::InitSkinningAndMorphPass()
+ void SkinnedMeshFeatureProcessor::InitSkinningAndMorphPass(const RPI::Ptr pipelineRootPass)
{
- m_skinningPass = nullptr; //reset it to null, just in case it fails to load the assets properly
- m_morphTargetPass = nullptr;
-
- RPI::PassSystemInterface* passSystem = RPI::PassSystemInterface::Get();
- if (passSystem->HasPassesForTemplateName(AZ::Name{ "SkinningPassTemplate" }))
+ RPI::Ptr skinningPass = pipelineRootPass->FindPassByNameRecursive(AZ::Name{ "SkinningPass" });
+ if (skinningPass)
{
- auto& skinningPasses = passSystem->GetPassesForTemplateName(AZ::Name{ "SkinningPassTemplate" });
+ SkinnedMeshComputePass* skinnedMeshComputePass = azdynamic_cast(skinningPass.get());
+ skinnedMeshComputePass->SetFeatureProcessor(this);
- // For now, assume one skinning pass
- if (!skinningPasses.empty() && skinningPasses[0])
- {
- m_skinningPass = static_cast(skinningPasses[0]);
- const Data::Instance shader = m_skinningPass->GetShader();
+ // There may be multiple skinning passes in the scene due to multiple pipelines, but there is only one skinning shader
+ m_skinningShader = skinnedMeshComputePass->GetShader();
- if (!shader)
- {
- AZ_Error(s_featureProcessorName, false, "Failed to get skinning pass shader. It may need to finish processing.");
- }
+ if (!m_skinningShader)
+ {
+ AZ_Error(s_featureProcessorName, false, "Failed to get skinning pass shader. It may need to finish processing.");
}
else
{
- AZ_Error(s_featureProcessorName, false, "\"SkinningPassTemplate\" does not have any valid passes. Check your game project's .pass assets.");
+ m_cachedSkinningShaderOptions.SetShader(m_skinningShader);
}
}
- else
- {
- AZ_Error(s_featureProcessorName, false, "Failed to find passes for \"SkinningPassTemplate\". Check your game project's .pass assets.");
- }
- if (passSystem->HasPassesForTemplateName(AZ::Name{ "MorphTargetPassTemplate" }))
+ RPI::Ptr morphTargetPass = pipelineRootPass->FindPassByNameRecursive(AZ::Name{ "MorphTargetPass" });
+ if (morphTargetPass)
{
- auto& morphTargetPasses = passSystem->GetPassesForTemplateName(AZ::Name{ "MorphTargetPassTemplate" });
+ MorphTargetComputePass* morphTargetComputePass = azdynamic_cast(morphTargetPass.get());
+ morphTargetComputePass->SetFeatureProcessor(this);
- // For now, assume one skinning pass
- if (!morphTargetPasses.empty() && morphTargetPasses[0])
- {
- m_morphTargetPass = static_cast(morphTargetPasses[0]);
- const Data::Instance shader = m_morphTargetPass->GetShader();
+ // There may be multiple morph target passes in the scene due to multiple pipelines, but there is only one morph target shader
+ m_morphTargetShader = morphTargetComputePass->GetShader();
- if (!shader)
- {
- AZ_Error(s_featureProcessorName, false, "Failed to get morph target pass shader. It may need to finish processing.");
- }
- }
- else
+ if (!m_morphTargetShader)
{
- AZ_Error(s_featureProcessorName, false, "\"MorphTargetPassTemplate\" does not have any valid passes. Check your game project's .pass assets.");
+ AZ_Error(s_featureProcessorName, false, "Failed to get morph target pass shader. It may need to finish processing.");
}
}
- else
+ }
+
+ RPI::ShaderOptionGroup SkinnedMeshFeatureProcessor::CreateSkinningShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler)
+ {
+ m_cachedSkinningShaderOptions.ConnectToShaderReinitializedEvent(shaderReinitializedHandler);
+ return m_cachedSkinningShaderOptions.CreateShaderOptionGroup(shaderOptions);
+ }
+
+ void SkinnedMeshFeatureProcessor::OnSkinningShaderReinitialized(const Data::Instance skinningShader)
+ {
+ m_skinningShader = skinningShader;
+ m_cachedSkinningShaderOptions.SetShader(m_skinningShader);
+ }
+
+ void SkinnedMeshFeatureProcessor::SubmitSkinningDispatchItems(RHI::CommandList* commandList)
+ {
+ AZStd::lock_guard lock(m_dispatchItemMutex);
+ for (const RHI::DispatchItem* dispatchItem : m_skinningDispatches)
+ {
+ commandList->Submit(*dispatchItem);
+ }
+ m_skinningDispatches.clear();
+ }
+
+ void SkinnedMeshFeatureProcessor::SubmitMorphTargetDispatchItems(RHI::CommandList* commandList)
+ {
+ AZStd::lock_guard lock(m_dispatchItemMutex);
+ for (const RHI::DispatchItem* dispatchItem : m_morphTargetDispatches)
{
- AZ_Error(s_featureProcessorName, false, "Failed to find passes for \"MorphTargetPassTemplate\". Check your game project's .pass assets.");
+ commandList->Submit(*dispatchItem);
}
+ m_morphTargetDispatches.clear();
}
SkinnedMeshRenderProxyInterfaceHandle SkinnedMeshFeatureProcessor::AcquireRenderProxyInterface(const SkinnedMeshRenderProxyDesc& desc)
@@ -363,14 +375,14 @@ namespace AZ
return ReleaseRenderProxy(handle);
}
- RPI::Ptr SkinnedMeshFeatureProcessor::GetSkinningPass() const
+ Data::Instance SkinnedMeshFeatureProcessor::GetSkinningShader() const
{
- return m_skinningPass;
+ return m_skinningShader;
}
- RPI::Ptr SkinnedMeshFeatureProcessor::GetMorphTargetPass() const
+ Data::Instance SkinnedMeshFeatureProcessor::GetMorphTargetShader() const
{
- return m_morphTargetPass;
+ return m_morphTargetShader;
}
} // namespace Render
} // namespace AZ
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
index 86dedfa161..75d41742b2 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.h
@@ -49,37 +49,47 @@ 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;
// RPI::SceneNotificationBus overrides ...
void OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) override;
- void OnRenderPipelineRemoved(RPI::RenderPipeline* pipeline) override;
void OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline) override;
void OnBeginPrepareRender() override;
- void OnEndPrepareRender() override;
SkinnedMeshRenderProxyHandle AcquireRenderProxy(const SkinnedMeshRenderProxyDesc& desc);
bool ReleaseRenderProxy(SkinnedMeshRenderProxyHandle& handle);
- RPI::Ptr GetSkinningPass() const;
- RPI::Ptr GetMorphTargetPass() const;
+ Data::Instance GetSkinningShader() const;
+ RPI::ShaderOptionGroup CreateSkinningShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler);
+ void OnSkinningShaderReinitialized(const Data::Instance skinningShader);
+ void SubmitSkinningDispatchItems(RHI::CommandList* commandList);
+
+ Data::Instance GetMorphTargetShader() const;
+ void SubmitMorphTargetDispatchItems(RHI::CommandList* commandList);
private:
AZ_DISABLE_COPY_MOVE(SkinnedMeshFeatureProcessor);
- void InitSkinningAndMorphPass();
+ void InitSkinningAndMorphPass(const RPI::Ptr pipelineRootPass);
SkinnedMeshRenderProxyInterfaceHandle AcquireRenderProxyInterface(const SkinnedMeshRenderProxyDesc& desc) override;
bool ReleaseRenderProxyInterface(SkinnedMeshRenderProxyInterfaceHandle& handle) override;
static const char* s_featureProcessorName;
- RPI::Ptr m_skinningPass;
- RPI::Ptr m_morphTargetPass;
+
+ Data::Instance m_skinningShader;
+ CachedSkinnedMeshShaderOptions m_cachedSkinningShaderOptions;
+
+ Data::Instance m_morphTargetShader;
+
AZStd::concurrency_checker m_renderProxiesChecker;
StableDynamicArray m_renderProxies;
AZStd::unique_ptr m_statsCollector;
MeshFeatureProcessor* m_meshFeatureProcessor = nullptr;
+ AZStd::unordered_set m_skinningDispatches;
+ AZStd::unordered_set m_morphTargetDispatches;
+ AZStd::mutex m_dispatchItemMutex;
};
} // namespace Render
diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshRenderProxy.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshRenderProxy.cpp
index 90c54dfc10..090b720406 100644
--- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshRenderProxy.cpp
+++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshRenderProxy.cpp
@@ -60,13 +60,7 @@ namespace AZ
bool SkinnedMeshRenderProxy::BuildDispatchItem([[maybe_unused]] const RPI::Scene& scene, size_t modelLodIndex, [[maybe_unused]] const SkinnedMeshShaderOptions& shaderOptions)
{
- if (!m_featureProcessor->GetSkinningPass())
- {
- AZ_Error("Skinned Mesh Feature Processor", false, "Failed to get Skinning Pass. Make sure the project has a skinning pass.");
- return false;
- }
-
- Data::Instance skinningShader = m_featureProcessor->GetSkinningPass()->GetShader();
+ Data::Instance skinningShader = m_featureProcessor->GetSkinningShader();
if (!skinningShader)
{
AZ_Error("Skinned Mesh Feature Processor", false, "Failed to get skinning shader from skinning pass");
@@ -89,7 +83,7 @@ namespace AZ
m_instance->m_outputStreamOffsetsInBytes[modelLodIndex],
modelLodIndex, m_boneTransforms,
m_shaderOptions,
- m_featureProcessor->GetSkinningPass(),
+ m_featureProcessor,
m_instance->m_morphTargetInstanceMetaData[modelLodIndex],
morphDeltaIntegerEncoding });
@@ -100,7 +94,7 @@ namespace AZ
}
// Get the data needed to create a morph target dispatch item
- Data::Instance morphTargetShader = m_featureProcessor->GetMorphTargetPass()->GetShader();
+ Data::Instance morphTargetShader = m_featureProcessor->GetMorphTargetShader();
const AZStd::vector>& morphTargetInputBuffersVector = m_inputBuffers->GetMorphTargetInputBuffers(modelLodIndex);
AZ_Assert(morphTargetMetaDatas.size() == morphTargetInputBuffersVector.size(), "Skinned Mesh Feature Processor - Mismatch in morph target metadata count and morph target input buffer count");
@@ -118,7 +112,7 @@ namespace AZ
aznew MorphTargetDispatchItem{
morphTargetInputBuffersVector[morphTargetIndex],
morphTargetMetaDatas[morphTargetIndex],
- m_featureProcessor->GetMorphTargetPass(),
+ m_featureProcessor,
m_instance->m_morphTargetInstanceMetaData[modelLodIndex],
morphDeltaIntegerEncoding });
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.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