diff --git a/Assets/Editor/Icons/PhysX/Move.svg b/Assets/Editor/Icons/PhysX/Move.svg deleted file mode 100644 index e9019bf226..0000000000 --- a/Assets/Editor/Icons/PhysX/Move.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets/Editor/Icons/PhysX/Rotate.svg b/Assets/Editor/Icons/PhysX/Rotate.svg deleted file mode 100644 index 79bfb77540..0000000000 --- a/Assets/Editor/Icons/PhysX/Rotate.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - Icons / Toolbar / Rotate - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Assets/Editor/Icons/PhysX/Scale.svg b/Assets/Editor/Icons/PhysX/Scale.svg deleted file mode 100644 index f4879b89f6..0000000000 --- a/Assets/Editor/Icons/PhysX/Scale.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - Icons / Toolbar / Scale - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Assets/Editor/Icons/WhiteBox/Move.svg b/Assets/Editor/Icons/WhiteBox/Move.svg deleted file mode 100644 index e9019bf226..0000000000 --- a/Assets/Editor/Icons/WhiteBox/Move.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Assets/Editor/Icons/WhiteBox/Rotate.svg b/Assets/Editor/Icons/WhiteBox/Rotate.svg deleted file mode 100644 index 79bfb77540..0000000000 --- a/Assets/Editor/Icons/WhiteBox/Rotate.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - Icons / Toolbar / Rotate - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Assets/Editor/Icons/WhiteBox/Scale.svg b/Assets/Editor/Icons/WhiteBox/Scale.svg deleted file mode 100644 index f4879b89f6..0000000000 --- a/Assets/Editor/Icons/WhiteBox/Scale.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - Icons / Toolbar / Scale - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/AutomatedTesting/CMakeLists.txt b/AutomatedTesting/CMakeLists.txt index e239ba7674..9a870b1279 100644 --- a/AutomatedTesting/CMakeLists.txt +++ b/AutomatedTesting/CMakeLists.txt @@ -10,7 +10,7 @@ # if(NOT PROJECT_NAME) - cmake_minimum_required(VERSION 3.19) + cmake_minimum_required(VERSION 3.20) project(AutomatedTesting LANGUAGES C CXX VERSION 1.0.0.0 diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py index 173d658b3c..3f94a23696 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py +++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/Menus_ViewMenuOptions.py @@ -49,7 +49,7 @@ class TestViewMenuOptions(EditorTestHelper): view_menu_options = [ ("Center on Selection",), ("Show Quick Access Bar",), - ("Viewport", "Wireframe"), + ("Viewport", "Configure Layout"), ("Viewport", "Go to Position"), ("Viewport", "Center on Selection"), ("Viewport", "Go to Location"), diff --git a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py index 2b3fdcbdf3..26231e33d7 100644 --- a/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py +++ b/AutomatedTesting/Gem/PythonTests/editor/test_Menus.py @@ -89,7 +89,7 @@ class TestMenus(object): expected_lines = [ "Center on Selection Action triggered", "Show Quick Access Bar Action triggered", - "Wireframe Action triggered", + "Configure Layout Action triggered", "Go to Position Action triggered", "Center on Selection Action triggered", "Go to Location Action triggered", diff --git a/AutomatedTesting/Gem/PythonTests/physics/C18243584_Joints_HingeSoftLimitsConstrained.py b/AutomatedTesting/Gem/PythonTests/physics/C18243584_Joints_HingeSoftLimitsConstrained.py index 90027be2d7..73800a54be 100755 --- a/AutomatedTesting/Gem/PythonTests/physics/C18243584_Joints_HingeSoftLimitsConstrained.py +++ b/AutomatedTesting/Gem/PythonTests/physics/C18243584_Joints_HingeSoftLimitsConstrained.py @@ -56,6 +56,7 @@ def C18243584_Joints_HingeSoftLimitsConstrained(): """ import os import sys + import math import ImportPathHelper as imports @@ -93,22 +94,51 @@ def C18243584_Joints_HingeSoftLimitsConstrained(): Report.info_vector3(lead.position, "lead initial position:") Report.info_vector3(follower.position, "follower initial position:") leadInitialPosition = lead.position - followerInitialPosition = follower.position - - # 4) Wait for several seconds - general.idle_wait(4.0) # wait for lead and follower to move - + + # 4) Wait for the follower to move above the lead or Timeout + normalizedStartPos = JointsHelper.getRelativeVector(lead.position, follower.position) + normalizedStartPos = normalizedStartPos.GetNormalizedSafe() + + class WaitCondition: + TARGET_ANGLE = math.radians(45) + TARGET_MAX_ANGLE = math.radians(180) + + angleAchieved = 0.0 + followerMovedAbove45Deg = False #this is expected to be true to pass the test + followerMovedAbove180Deg = True #this is expected to be false to pass the test + + def checkConditionMet(self): + #calculate the current follower-lead vector + normalVec = JointsHelper.getRelativeVector(lead.position, follower.position) + normalVec = normalVec.GetNormalizedSafe() + #dot product + acos to get the angle + currentAngle = math.acos(normalizedStartPos.Dot(normalVec)) + #if the angle is now less then last time, it is no longer rising, so end the test. + if currentAngle < self.angleAchieved: + return True + + self.angleAchieved = currentAngle + self.followerMovedAbove45Deg = currentAngle > self.TARGET_ANGLE + self.followerMovedAbove180Deg = currentAngle > self.TARGET_MAX_ANGLE + return False + + def isFollowerPositionCorrect(self): + return self.followerMovedAbove45Deg and not self.followerMovedAbove180Deg + + waitCondition = WaitCondition() + + MAX_WAIT_TIME = 5.0 #seconds + conditionMet = helper.wait_for_condition(lambda: waitCondition.checkConditionMet(), MAX_WAIT_TIME) + # 5) Check to see if lead and follower behaved as expected - Report.info_vector3(lead.position, "lead position after 1 second:") - Report.info_vector3(follower.position, "follower position after 1 second:") + Report.info_vector3(lead.position, "lead position after test:") + Report.info_vector3(follower.position, "follower position after test:") leadPositionDelta = lead.position.Subtract(leadInitialPosition) leadRemainedStill = JointsHelper.vector3SmallerThanScalar(leadPositionDelta, FLOAT_EPSILON) Report.critical_result(Tests.check_lead_position, leadRemainedStill) - followerMovedInXOnly = ((follower.position.x > leadInitialPosition.x) > FLOAT_EPSILON and - (follower.position.z - leadInitialPosition.z) > FLOAT_EPSILON) - Report.critical_result(Tests.check_follower_position, followerMovedInXOnly) + Report.critical_result(Tests.check_follower_position, conditionMet and waitCondition.isFollowerPositionCorrect()) # 6) Exit Game Mode helper.exit_game_mode(Tests.exit_game_mode) diff --git a/AutomatedTesting/Levels/Physics/C18243584_Joints_HingeSoftLimitsConstrained/C18243584_Joints_HingeSoftLimitsConstrained.ly b/AutomatedTesting/Levels/Physics/C18243584_Joints_HingeSoftLimitsConstrained/C18243584_Joints_HingeSoftLimitsConstrained.ly index 585ca394f1..2307843887 100644 --- a/AutomatedTesting/Levels/Physics/C18243584_Joints_HingeSoftLimitsConstrained/C18243584_Joints_HingeSoftLimitsConstrained.ly +++ b/AutomatedTesting/Levels/Physics/C18243584_Joints_HingeSoftLimitsConstrained/C18243584_Joints_HingeSoftLimitsConstrained.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16f592487e8973abcf6b696aee6e430924c22daac0d6bb781c9e76153cd932f7 -size 8885 +oid sha256:352a64f523b3246000393309fa7f14955fe554e0b792a4177349f0f2db8a2b62 +size 5901 diff --git a/AutomatedTesting/Levels/Physics/C18243586_Joints_HingeLeadFollowerCollide/C18243586_Joints_HingeLeadFollowerCollide.ly b/AutomatedTesting/Levels/Physics/C18243586_Joints_HingeLeadFollowerCollide/C18243586_Joints_HingeLeadFollowerCollide.ly index d5675c2542..83ba9e3831 100644 --- a/AutomatedTesting/Levels/Physics/C18243586_Joints_HingeLeadFollowerCollide/C18243586_Joints_HingeLeadFollowerCollide.ly +++ b/AutomatedTesting/Levels/Physics/C18243586_Joints_HingeLeadFollowerCollide/C18243586_Joints_HingeLeadFollowerCollide.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d5c38cf9b97ae28c391916e6637aebf767d5ac009df61e730396fe8b116f3e5 -size 6913 +oid sha256:31bd1feb92c3bb8a5c5df4638927a9a80e879329965732c4c32f673c236a8b0a +size 6021 diff --git a/AutomatedTesting/Levels/Physics/C18243589_Joints_BallSoftLimitsConstrained/C18243589_Joints_BallSoftLimitsConstrained.ly b/AutomatedTesting/Levels/Physics/C18243589_Joints_BallSoftLimitsConstrained/C18243589_Joints_BallSoftLimitsConstrained.ly index 6091c9e137..ecd4b61cb0 100644 --- a/AutomatedTesting/Levels/Physics/C18243589_Joints_BallSoftLimitsConstrained/C18243589_Joints_BallSoftLimitsConstrained.ly +++ b/AutomatedTesting/Levels/Physics/C18243589_Joints_BallSoftLimitsConstrained/C18243589_Joints_BallSoftLimitsConstrained.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6f26f1c1c037fa0b848ac9b3d9a76e476b4853d770f1a77c01714286733b567 -size 6921 +oid sha256:063779c1e80ce22319cb82ff0d7635d3dcbb043b789c3830cc405ffa36d3c0ef +size 5876 diff --git a/AutomatedTesting/Levels/Physics/C18243591_Joints_BallLeadFollowerCollide/C18243591_Joints_BallLeadFollowerCollide.ly b/AutomatedTesting/Levels/Physics/C18243591_Joints_BallLeadFollowerCollide/C18243591_Joints_BallLeadFollowerCollide.ly index e50ffc9dff..3991492730 100644 --- a/AutomatedTesting/Levels/Physics/C18243591_Joints_BallLeadFollowerCollide/C18243591_Joints_BallLeadFollowerCollide.ly +++ b/AutomatedTesting/Levels/Physics/C18243591_Joints_BallLeadFollowerCollide/C18243591_Joints_BallLeadFollowerCollide.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ffd3c4ee04fa8a414995c39c7ca79246e3d9b0ceee1ad1b85d61a5298f71495 -size 6940 +oid sha256:5bd3a952841aa924a4869c74fad7ed397667a87948270f0ce35f314e6cf2e14a +size 5927 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ca9aeacb8..4a12e0c50a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,17 +9,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -# Cmake version 3.19 is the minimum version needed for all of Open 3D Engine's supported platforms -cmake_minimum_required(VERSION 3.19) - -# CMP0111 introduced in 3.19 has a bug that produces the policy to warn every time there is an -# INTERFACE IMPORTED library. We use this type of libraries for handling 3rdParty. The rest of -# the documentation states that INTERFACE IMPORTED libraries do not require to set locations, but -# the policy still warns about it. Issue: https://gitlab.kitware.com/cmake/cmake/-/issues/21470 -# The issue was fixed in 3.19.1 so we just disable the policy for 3.19 -if(CMAKE_VERSION VERSION_EQUAL 3.19) - cmake_policy(SET CMP0111 OLD) -endif() +# Cmake version 3.20 is the minimum version needed for all of Open 3D Engine's supported platforms +cmake_minimum_required(VERSION 3.20) include(cmake/LySet.cmake) include(cmake/Version.cmake) @@ -114,24 +105,25 @@ endforeach() # Post-processing ################################################################################ # The following steps have to be done after all targets are registered: -# Defer generation of the StaticModules.inl file which is needed to create the AZ::Module derived class in monolithic -# builds until after all the targets are known -ly_delayed_generate_static_modules_inl() # 1. Add any dependencies registered via ly_enable_gems ly_enable_gems_delayed() -# 2. generate a settings registry .setreg file for all ly_add_project_dependencies() and ly_add_target_dependencies() calls +# 2. Defer generation of the StaticModules.inl file which is needed to create the AZ::Module derived class in monolithic +# builds until after all the targets are known and all the gems are enabled +ly_delayed_generate_static_modules_inl() + +# 3. generate a settings registry .setreg file for all ly_add_project_dependencies() and ly_add_target_dependencies() calls # to provide applications with the filenames of gem modules to load # This must be done before ly_delayed_target_link_libraries() as that inserts BUILD_DEPENDENCIES as MANUALLY_ADDED_DEPENDENCIES # if the build dependency is a MODULE_LIBRARY. That would cause a false load dependency to be generated ly_delayed_generate_settings_registry() -# 3. link targets where the dependency was yet not declared, we need to have the declaration so we do different +# 4. link targets where the dependency was yet not declared, we need to have the declaration so we do different # linking logic depending on the type of target ly_delayed_target_link_libraries() -# 4. generate a registry file for unit testing for platforms that support unit testing +# 5. generate a registry file for unit testing for platforms that support unit testing if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_delayed_generate_unit_test_module_registry() endif() diff --git a/Code/CryEngine/CryCommon/MTPseudoRandom.cpp b/Code/CryEngine/CryCommon/MTPseudoRandom.cpp deleted file mode 100644 index 0796d92f66..0000000000 --- a/Code/CryEngine/CryCommon/MTPseudoRandom.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ -// Original file Copyright Crytek GMBH or its affiliates, used under license. - -// Description : Marsenne Twister PRNG. See MT.h for more info. - -#include "MTPseudoRandom.h" -// non-inline function definitions and static member definitions cannot -// reside in header file because of the risk of multiple declarations - -void CMTRand_int32::gen_state() // generate new m_nState vector -{ - for (int i = 0; i < (n - m); ++i) - { - m_nState[i] = m_nState[i + m] ^ twiddle(m_nState[i], m_nState[i + 1]); - } - for (int i = n - m; i < (n - 1); ++i) - { - m_nState[i] = m_nState[i + m - n] ^ twiddle(m_nState[i], m_nState[i + 1]); - } - m_nState[n - 1] = m_nState[m - 1] ^ twiddle(m_nState[n - 1], m_nState[0]); - p = 0; // reset position -} - -void CMTRand_int32::seed(uint32 s) // init by 32 bit seed -{ //if (s == 0) - //m_nRandom = 1; - for (int i = 0; i < n; ++i) - { - m_nState[i] = 0x0UL; - } - m_nState[0] = s; - for (int i = 1; i < n; ++i) - { - m_nState[i] = 1812433253UL * (m_nState[i - 1] ^ (m_nState[i - 1] >> 30)) + i; - // see Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier - // in the previous versions, MSBs of the seed affect only MSBs of the array m_nState - // 2002/01/09 modified by Makoto Matsumoto - } - p = n; // force gen_state() to be called for next random number -} - -void CMTRand_int32::seed(const uint32* array, int size) // init by array -{ - seed(19650218UL); - int i = 1, j = 0; - for (int k = ((n > size) ? n : size); k; --k) - { - m_nState[i] = (m_nState[i] ^ ((m_nState[i - 1] ^ (m_nState[i - 1] >> 30)) * 1664525UL)) - + array[j] + j; // non linear - ++j; - j %= size; - if ((++i) == n) - { - m_nState[0] = m_nState[n - 1]; - i = 1; - } - } - for (int k = n - 1; k; --k) - { - PREFAST_SUPPRESS_WARNING(6385) PREFAST_SUPPRESS_WARNING(6386) m_nState[i] = (m_nState[i] ^ ((m_nState[i - 1] ^ (m_nState[i - 1] >> 30)) * 1566083941UL)) - i; - if ((++i) == n) - { - m_nState[0] = m_nState[n - 1]; - i = 1; - } - } - m_nState[0] = 0x80000000UL; // MSB is 1; assuring non-zero initial array - p = n; // force gen_state() to be called for next random number -} diff --git a/Code/CryEngine/CryCommon/MTPseudoRandom.h b/Code/CryEngine/CryCommon/MTPseudoRandom.h deleted file mode 100644 index 40075cf167..0000000000 --- a/Code/CryEngine/CryCommon/MTPseudoRandom.h +++ /dev/null @@ -1,166 +0,0 @@ -// mtrand.h -// C++ include file for MT19937, with initialization improved 2002/1/26. -// Coded by Takuji Nishimura and Makoto Matsumoto. -// Ported to C++ by Jasper Bedaux 2003/1/1 (see http://www.bedaux.net/mtrand/). -// The generators returning floating point numbers are based on -// a version by Isaku Wada, 2002/01/09 -// Static shared data converted to per-instance, 2008-11-13 by JSP. -// -// Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// -// 1. Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// 3. The names of its contributors may not be used to endorse or promote -// products derived from this software without specific prior written -// permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Any feedback is very welcome. -// http://www.math.keio.ac.jp/matumoto/emt.html -// email: matumoto@math.keio.ac.jp -// -// Feedback about the C++ port should be sent to Jasper Bedaux, -// see http://www.bedaux.net/mtrand/ for e-mail address and info. - -//------------------------------------------------------------------------- -// History: -// - 28:7:2005: File created and minor changes by Marco Corbetta -// -//*************************************************************************/ -// Modifications copyright Amazon.com, Inc. or its affiliates - -#ifndef CRYINCLUDE_CRYCOMMON_MTPSEUDORANDOM_H -#define CRYINCLUDE_CRYCOMMON_MTPSEUDORANDOM_H -#pragma once - -#include - -////////////////////////////////////////////////////////////////////////// -class CMTRand_int32 -{ - // Mersenne Twister random number generator - -public: - // default constructor - CMTRand_int32() { seed(5489UL); } - // constructor with 32 bit int as seed - CMTRand_int32(uint32 seed_value) { seed(seed_value); } - // constructor with array of 32 bit integers as seed - CMTRand_int32(const uint32* array, int size) { seed(array, size); } - // seeds with 32 bit integer - void seed(uint32 seed_value); - // seeds with array - void seed(const uint32*, int size); - // overloaded operator() to make this a generator (functor) - //uint32 operator()() { return rand_int32(); } - - ~CMTRand_int32() {} - - // Functions with PascalCase names were added for - // interchangeability with CRndGen (see LCGRandom.h). - - void Seed(uint32 seed_value) - { - seed(seed_value); - } - - uint32 GenerateUint32() - { - return rand_int32(); - } - - uint64 GenerateUint64() - { - const uint32 a = GenerateUint32(); - const uint32 b = GenerateUint32(); - return ((uint64)b << 32) | (uint64)a; - } - - float GenerateFloat() - { - return (float)GenerateUint32() * (1.0f / 4294967295.0f); - } - - // Ranged function returns random value within the *inclusive* range - // between minValue and maxValue. - // Any orderings work correctly: minValue <= maxValue and - // minValue >= minValue. - template - T GetRandom(const T minValue, const T maxValue) - { - return CryRandom_Internal::BoundedRandom::Get(*this, minValue, maxValue); - } - - // Vector (Vec2, Vec3, Vec4) ranged function returns vector with - // every component within the *inclusive* ranges between minValue.component - // and maxValue.component. - // All orderings work correctly: minValue.component <= maxValue.component and - // minValue.component >= maxValue.component. - template - T GetRandomComponentwise(const T& minValue, const T& maxValue) - { - return CryRandom_Internal::BoundedRandomComponentwise::Get(*this, minValue, maxValue); - } - - // The function returns a random unit vector (Vec2, Vec3, Vec4). - template - T GetRandomUnitVector() - { - return CryRandom_Internal::GetRandomUnitVector(*this); - } - -protected: // used by derived classes, otherwise not accessible; use the ()-operator - // generates 32 bit random int - uint32 rand_int32() - { - if (p >= n) gen_state(); // new m_nState vector needed - // gen_state() is split off to be non-inline, because it is only called once - // in every 624 calls and otherwise irand() would become too big to get inlined - uint32 x = m_nState[p++]; - x ^= (x >> 11); - x ^= (x << 7) & 0x9D2C5680UL; - x ^= (x << 15) & 0xEFC60000UL; - return x ^ (x >> 18); - } - -private: - static const int n = 624, m = 397; // compile time constants - - // the variables below are static (no duplicates can exist) - uint32 m_nState[n+1]; // m_nState vector array - int p; // position in m_nState array - // private functions used to generate the pseudo random numbers - uint32 twiddle(uint32 u, uint32 v) - { - return (((u & 0x80000000UL) | (v & 0x7FFFFFFFUL)) >> 1) - ^ ((v & 1UL) ? 0x9908B0DFUL : 0x0UL); - } - void gen_state(); // generate new m_nState - // make copy constructor and assignment operator unavailable, they don't make sense - CMTRand_int32(const CMTRand_int32&); // copy constructor not defined - void operator=(const CMTRand_int32&); // assignment operator not defined -}; - - -#endif // CRYINCLUDE_CRYCOMMON_MTPSEUDORANDOM_H diff --git a/Code/CryEngine/CryCommon/Random.h b/Code/CryEngine/CryCommon/Random.h index 0f0a9f0e89..6de9136113 100644 --- a/Code/CryEngine/CryCommon/Random.h +++ b/Code/CryEngine/CryCommon/Random.h @@ -17,7 +17,6 @@ #include "BaseTypes.h" #include "LCGRandom.h" -#include "MTPseudoRandom.h" namespace CryRandom_Internal { diff --git a/Code/CryEngine/CryCommon/crycommon_files.cmake b/Code/CryEngine/CryCommon/crycommon_files.cmake index 660cf20276..567bbefca7 100644 --- a/Code/CryEngine/CryCommon/crycommon_files.cmake +++ b/Code/CryEngine/CryCommon/crycommon_files.cmake @@ -69,7 +69,6 @@ set(FILES CryRandomInternal.h Random.h LCGRandom.h - MTPseudoRandom.cpp CryTypeInfo.cpp BaseTypes.h CompileTimeAssert.h @@ -102,7 +101,6 @@ set(FILES LegacyAllocator.h MetaUtils.h MiniQueue.h - MTPseudoRandom.h MultiThread.h MultiThread_Containers.h NullAudioSystem.h diff --git a/Code/CryEngine/CrySystem/LevelSystem/LevelSystem.cpp b/Code/CryEngine/CrySystem/LevelSystem/LevelSystem.cpp index aa5d638831..708835c0cc 100644 --- a/Code/CryEngine/CrySystem/LevelSystem/LevelSystem.cpp +++ b/Code/CryEngine/CrySystem/LevelSystem/LevelSystem.cpp @@ -321,7 +321,7 @@ void CLevelSystem::ScanFolder(const char* subfolder, bool modFolder) { if (AZ::StringFunc::Equal(handle.m_filename.data(), LevelPakName)) { - // level folder contain pak files like 'level.pak' + // level folder contain pak files like 'level.pak' // which we only want to load during level loading. continue; } @@ -352,7 +352,7 @@ void CLevelSystem::ScanFolder(const char* subfolder, bool modFolder) PopulateLevels(search, folder, pPak, modFolder, false); // Load levels outside of the bundles to maintain backward compatibility. PopulateLevels(search, folder, pPak, modFolder, true); - + } void CLevelSystem::PopulateLevels( @@ -974,7 +974,7 @@ void CLevelSystem::UnloadLevel() m_lastLevelName.clear(); SAFE_RELEASE(m_pCurrentLevel); - + // Force Lua garbage collection (may no longer be needed now the legacy renderer has been removed). // Normally the GC step is triggered at the end of this method (by the ESYSTEM_EVENT_LEVEL_POST_UNLOAD event). EBUS_EVENT(AZ::ScriptSystemRequestBus, GarbageCollect); diff --git a/Code/Framework/AzAutoGen/CMakeLists.txt b/Code/Framework/AzAutoGen/CMakeLists.txt index 925e7aed29..4c79f268fe 100644 --- a/Code/Framework/AzAutoGen/CMakeLists.txt +++ b/Code/Framework/AzAutoGen/CMakeLists.txt @@ -9,8 +9,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -cmake_minimum_required(VERSION 3.0) - ly_add_target( NAME AzAutoGen HEADERONLY NAMESPACE AZ diff --git a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathCommon_simd.inl b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathCommon_simd.inl index 770a8fa104..d06372256d 100644 --- a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathCommon_simd.inl +++ b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathCommon_simd.inl @@ -292,8 +292,13 @@ namespace AZ const typename VecType::FloatType cmp2 = VecType::AndNot(cmp0, cmp1); // -1/x + // this step is calculated for all values of x, but only used if x > Sqrt(2) + 1 + // in order to avoid a division by zero, detect if xabs is zero here and replace it with an arbitrary value + // if xabs does equal zero, the value here doesn't matter because the result will be thrown away + typename VecType::FloatType xabsSafe = + VecType::Add(xabs, VecType::And(VecType::CmpEq(xabs, VecType::ZeroFloat()), FastLoadConstant(Simd::g_vec1111))); const typename VecType::FloatType y0 = VecType::And(cmp0, FastLoadConstant(Simd::g_HalfPi)); - typename VecType::FloatType x0 = VecType::Div(FastLoadConstant(Simd::g_vec1111), xabs); + typename VecType::FloatType x0 = VecType::Div(FastLoadConstant(Simd::g_vec1111), xabsSafe); x0 = VecType::Xor(x0, VecType::CastToFloat(FastLoadConstant(Simd::g_negateMask))); const typename VecType::FloatType y1 = VecType::And(cmp2, FastLoadConstant(Simd::g_QuarterPi)); @@ -368,8 +373,12 @@ namespace AZ typename VecType::FloatType offset = VecType::And(x_lt_0, offset1); + // the result of this part of the computation is thrown away if x equals 0, + // but if x does equal 0, it will cause a division by zero + // so replace zero by an arbitrary value here in that case + typename VecType::FloatType xSafe = VecType::Add(x, VecType::And(x_eq_0, FastLoadConstant(Simd::g_vec1111))); const typename VecType::FloatType atan_mask = VecType::Not(VecType::Or(x_eq_0, y_eq_0)); - const typename VecType::FloatType atan_arg = VecType::Div(y, x); + const typename VecType::FloatType atan_arg = VecType::Div(y, xSafe); typename VecType::FloatType atan_result = VecType::Atan(atan_arg); atan_result = VecType::Add(atan_result, offset); atan_result = VecType::AndNot(pio2_mask, atan_result); diff --git a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec2_sse.inl b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec2_sse.inl index 8f35af258c..63e2dca8fd 100644 --- a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec2_sse.inl +++ b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec2_sse.inl @@ -471,6 +471,7 @@ namespace AZ AZ_MATH_INLINE Vec2::FloatType Vec2::Reciprocal(FloatArgType value) { + value = Sse::ReplaceFourth(Sse::ReplaceThird(value, 1.0f), 1.0f); return Sse::Reciprocal(value); } @@ -513,6 +514,7 @@ namespace AZ AZ_MATH_INLINE Vec2::FloatType Vec2::SqrtInv(FloatArgType value) { + value = Sse::ReplaceFourth(Sse::ReplaceThird(value, 1.0f), 1.0f); return Sse::SqrtInv(value); } diff --git a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec3_sse.inl b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec3_sse.inl index 78a0d5db66..75ee2ab7c5 100644 --- a/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec3_sse.inl +++ b/Code/Framework/AzCore/AzCore/Math/Internal/SimdMathVec3_sse.inl @@ -507,6 +507,7 @@ namespace AZ AZ_MATH_INLINE Vec3::FloatType Vec3::Reciprocal(FloatArgType value) { + value = Sse::ReplaceFourth(value, 1.0f); return Sse::Reciprocal(value); } @@ -549,6 +550,7 @@ namespace AZ AZ_MATH_INLINE Vec3::FloatType Vec3::SqrtInv(FloatArgType value) { + value = Sse::ReplaceFourth(value, 1.0f); return Sse::SqrtInv(value); } diff --git a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp index 7025b3977e..e3647ec2cb 100644 --- a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp +++ b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp @@ -175,4 +175,11 @@ namespace AZ::Utils path /= ".o3de"; return path.Native(); } + + AZ::IO::FixedMaxPathString GetO3deLogsDirectory() + { + AZ::IO::FixedMaxPath path = GetO3deManifestDirectory(); + path /= "Logs"; + return path.Native(); + } } diff --git a/Code/Framework/AzCore/AzCore/Utils/Utils.h b/Code/Framework/AzCore/AzCore/Utils/Utils.h index d082a3ebe0..8fb6f05742 100644 --- a/Code/Framework/AzCore/AzCore/Utils/Utils.h +++ b/Code/Framework/AzCore/AzCore/Utils/Utils.h @@ -97,6 +97,9 @@ namespace AZ //! Retrieves the full path where the manifest file lives, i.e. "/.o3de/o3de_manifest.json" AZ::IO::FixedMaxPathString GetEngineManifestPath(); + //! Retrieves the full directory to the O3DE logs directory, i.e. "/.o3de/Logs" + AZ::IO::FixedMaxPathString GetO3deLogsDirectory(); + //! Retrieves the App root path to use on the current platform //! If the optional is not engaged the AppRootPath should be calculated based //! on the location of the bootstrap.cfg file diff --git a/Code/Framework/AzCore/AzCore/std/math.h b/Code/Framework/AzCore/AzCore/std/math.h index f5e2ac7ea7..fe2469eb41 100644 --- a/Code/Framework/AzCore/AzCore/std/math.h +++ b/Code/Framework/AzCore/AzCore/std/math.h @@ -26,6 +26,7 @@ namespace AZStd using std::exp2; using std::floor; using std::fmod; + using std::pow; using std::round; using std::sin; using std::sqrt; diff --git a/Code/Framework/AzCore/Tests/Components.cpp b/Code/Framework/AzCore/Tests/Components.cpp index 7801f69375..9d9bd46d3d 100644 --- a/Code/Framework/AzCore/Tests/Components.cpp +++ b/Code/Framework/AzCore/Tests/Components.cpp @@ -1555,7 +1555,9 @@ namespace UnitTest } } - TEST_F(Components, EntityIdGeneration) + // Temporary disabled. This will be re-enabled in the short term upon completion of SPEC-7384 and + // fixed in the long term upon completion of SPEC-4849 + TEST_F(Components, DISABLED_EntityIdGeneration) { // Generate 1 million ids across 100 threads, and ensure that none collide AZStd::concurrent_unordered_set entityIds; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.cpp new file mode 100644 index 0000000000..c654038ead --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.cpp @@ -0,0 +1,36 @@ +/* +* 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 + +namespace AzPhysics +{ + AZ_CLASS_ALLOCATOR_IMPL(Joint, AZ::SystemAllocator, 0); + + void Joint::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azdynamic_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("SceneOwner", &Joint::m_sceneOwner) + ->Field("JointHandle", &Joint::m_jointHandle) + ; + } + } +} diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.h b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.h new file mode 100644 index 0000000000..a73a9e5cd4 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsJoint.h @@ -0,0 +1,143 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + class ReflectContext; +} + +namespace AzPhysics +{ + struct JointConfiguration; + + //! Base class for all Joints in Physics. + struct Joint + { + AZ_CLASS_ALLOCATOR_DECL; + AZ_RTTI(AzPhysics::Joint, "{1EEC9382-3434-4866-9B18-E93F151A6F59}"); + static void Reflect(AZ::ReflectContext* context); + + virtual ~Joint() = default; + + //! The current Scene the joint is contained. + SceneHandle m_sceneOwner = AzPhysics::InvalidSceneHandle; + + //! The handle to this joint. + JointHandle m_jointHandle = AzPhysics::InvalidJointHandle; + + //! Helper functions for setting user data. + //! @param userData Can be a pointer to any type as internally will be cast to a void*. Object lifetime not managed by the Joint. + template + void SetUserData(T* userData) + { + m_customUserData = static_cast(userData); + } + //! Helper functions for getting the set user data. + //! @return Will return a void* to the user data set. + void* GetUserData() + { + return m_customUserData; + } + + virtual AZ::Crc32 GetNativeType() const = 0; + virtual void* GetNativePointer() const = 0; + + virtual AzPhysics::SimulatedBodyHandle GetParentBodyHandle() const = 0; + virtual AzPhysics::SimulatedBodyHandle GetChildBodyHandle() const = 0; + + virtual void SetParentBody(AzPhysics::SimulatedBodyHandle parentBody) = 0; + virtual void SetChildBody(AzPhysics::SimulatedBodyHandle childBody) = 0; + + virtual void GenerateJointLimitVisualizationData( + [[ maybe_unused ]] float scale, + [[ maybe_unused ]] AZ::u32 angularSubdivisions, + [[ maybe_unused ]] AZ::u32 radialSubdivisions, + [[ maybe_unused ]] AZStd::vector& vertexBufferOut, + [[ maybe_unused ]] AZStd::vector& indexBufferOut, + [[ maybe_unused ]] AZStd::vector& lineBufferOut, + [[ maybe_unused ]] AZStd::vector& lineValidityBufferOut) { } + + private: + void* m_customUserData = nullptr; + }; + + //! Alias for a list of non owning weak pointers to Joint objects. + using JointList = AZStd::vector; + + //! Interface to access Joint utilities and helper functions + class JointHelpersInterface + { + public: + AZ_RTTI(AzPhysics::JointHelpersInterface, "{A511C64D-C8A5-4E8F-9C69-8DC5EFAD0C4C}"); + + JointHelpersInterface() = default; + virtual ~JointHelpersInterface() = default; + AZ_DISABLE_COPY_MOVE(JointHelpersInterface); + + //! Returns a list of supported Joint types + virtual const AZStd::vector GetSupportedJointTypeIds() const = 0; + + //! Returns a TypeID if the request joint type is supported. + //! If the Physics backend supports this joint type JointHelpersInterface::GetSupportedJointTypeId will return a AZ::TypeId. + virtual AZStd::optional GetSupportedJointTypeId(JointType typeEnum) const = 0; + + //! Computes parameters such as joint limit local rotations to give the desired initial joint limit orientation. + //! @param jointLimitTypeId The type ID used to identify the particular kind of joint limit configuration to be created. + //! @param parentWorldRotation The rotation in world space of the parent world body associated with the joint. + //! @param childWorldRotation The rotation in world space of the child world body associated with the joint. + //! @param axis Axis used to define the centre for limiting angular degrees of freedom. + //! @param exampleLocalRotations A vector (which may be empty) containing example valid rotations in the local space + //! of the child world body relative to the parent world body, which may optionally be used to help estimate the extents + //! of the joint limit. + virtual AZStd::unique_ptr ComputeInitialJointLimitConfiguration( + const AZ::TypeId& jointLimitTypeId, + const AZ::Quaternion& parentWorldRotation, + const AZ::Quaternion& childWorldRotation, + const AZ::Vector3& axis, + const AZStd::vector& exampleLocalRotations) = 0; + + /// Generates joint limit visualization data in appropriate format to pass to DebugDisplayRequests draw functions. + /// @param configuration The joint configuration to generate visualization data for. + /// @param parentRotation The rotation of the joint's parent body (in the same frame as childRotation). + /// @param childRotation The rotation of the joint's child body (in the same frame as parentRotation). + /// @param scale Scale factor for the output display data. + /// @param angularSubdivisions Level of detail in the angular direction (may be clamped in the implementation). + /// @param radialSubdivisions Level of detail in the radial direction (may be clamped in the implementation). + /// @param[out] vertexBufferOut Used with indexBufferOut to define triangles to be displayed. + /// @param[out] indexBufferOut Used with vertexBufferOut to define triangles to be displayed. + /// @param[out] lineBufferOut Used to define lines to be displayed. + /// @param[out] lineValidityBufferOut Whether each line in the line buffer is part of a valid or violated limit. + virtual void GenerateJointLimitVisualizationData( + const JointConfiguration& configuration, + const AZ::Quaternion& parentRotation, + const AZ::Quaternion& childRotation, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& vertexBufferOut, + AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) = 0; + }; +} diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsTypes.h b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsTypes.h index b2b5fd1511..42aee35c2c 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsTypes.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsTypes.h @@ -51,8 +51,10 @@ namespace AzPhysics using SceneIndex = AZ::s8; using SimulatedBodyIndex = AZ::s32; + using JointIndex = AZ::s32; static_assert(std::is_signed::value - && std::is_signed::value, "SceneIndex and SimulatedBodyIndex must be signed integers."); + && std::is_signed::value + && std::is_signed::value, "SceneIndex, SimulatedBodyIndex and JointIndex must be signed integers."); //! A handle to a Scene within the physics simulation. @@ -69,12 +71,27 @@ namespace AzPhysics static constexpr SimulatedBodyHandle InvalidSimulatedBodyHandle = { AZ::Crc32(), -1 }; using SimulatedBodyHandleList = AZStd::vector; + //! A handle to a Joint within a physics scene. + //! A JointHandle is a tuple of a Crc of the scene's name and the index in the Joint list. + using JointHandle = AZStd::tuple; + static constexpr JointHandle InvalidJointHandle = { AZ::Crc32(), -1 }; + //! Helper used for pairing the ShapeConfiguration and ColliderConfiguration together which is used when creating a Simulated Body. using ShapeColliderPair = AZStd::pair< AZStd::shared_ptr, AZStd::shared_ptr>; using ShapeColliderPairList = AZStd::vector; + //! Joint types are used to request for AZ::TypeId with the JointHelpersInterface::GetSupportedJointTypeId. + //! If the Physics backend supports this joint type JointHelpersInterface::GetSupportedJointTypeId will return a AZ::TypeId. + enum class JointType + { + D6Joint, + FixedJoint, + BallJoint, + HingeJoint + }; + //! Flags used to specifying which properties of a body to compute. enum class MassComputeFlags : AZ::u8 { diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp new file mode 100644 index 0000000000..aa58136944 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp @@ -0,0 +1,37 @@ +/* +* 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 AzPhysics +{ + AZ_CLASS_ALLOCATOR_IMPL(JointConfiguration, AZ::SystemAllocator, 0); + + void JointConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("Name", &JointConfiguration::m_debugName) + ->Field("ParentLocalRotation", &JointConfiguration::m_parentLocalRotation) + ->Field("ParentLocalPosition", &JointConfiguration::m_parentLocalPosition) + ->Field("ChildLocalRotation", &JointConfiguration::m_childLocalRotation) + ->Field("ChildLocalPosition", &JointConfiguration::m_childLocalPosition) + ->Field("StartSimulationEnabled", &JointConfiguration::m_startSimulationEnabled) + ; + } + } +} diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h new file mode 100644 index 0000000000..8ebe1199a8 --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h @@ -0,0 +1,51 @@ +/* +* 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 AZ +{ + class ReflectContext; +} + +namespace AzPhysics +{ + //! Base Class of all Physics Joints that will be simulated. + struct JointConfiguration + { + AZ_CLASS_ALLOCATOR_DECL; + AZ_RTTI(AzPhysics::JointConfiguration, "{DF91D39A-4901-48C4-9159-93FD2ACA5252}"); + static void Reflect(AZ::ReflectContext* context); + + JointConfiguration() = default; + virtual ~JointConfiguration() = default; + + // Entity/object association. + void* m_customUserData = nullptr; + + // Basic initial settings. + AZ::Quaternion m_parentLocalRotation = AZ::Quaternion::CreateIdentity(); ///< Parent joint frame relative to parent body. + AZ::Vector3 m_parentLocalPosition = AZ::Vector3::CreateZero(); ///< Joint position relative to parent body. + AZ::Quaternion m_childLocalRotation = AZ::Quaternion::CreateIdentity(); ///< Child joint frame relative to child body. + AZ::Vector3 m_childLocalPosition = AZ::Vector3::CreateZero(); ///< Joint position relative to child body. + bool m_startSimulationEnabled = true; + + // For debugging/tracking purposes only. + AZStd::string m_debugName; + }; +} diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Joint.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Joint.cpp deleted file mode 100644 index 965583c113..0000000000 --- a/Code/Framework/AzFramework/AzFramework/Physics/Joint.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#include -#include -#include -#include - -namespace Physics -{ - const char* JointLimitConfiguration::GetTypeName() - { - return "Base Joint"; - } - - void JointLimitConfiguration::Reflect(AZ::ReflectContext* context) - { - if (auto serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("ParentLocalRotation", &JointLimitConfiguration::m_parentLocalRotation) - ->Field("ParentLocalPosition", &JointLimitConfiguration::m_parentLocalPosition) - ->Field("ChildLocalRotation", &JointLimitConfiguration::m_childLocalRotation) - ->Field("ChildLocalPosition", &JointLimitConfiguration::m_childLocalPosition) - ; - - AZ::EditContext* editContext = serializeContext->GetEditContext(); - if (editContext) - { - editContext->Class( - "Joint Configuration", "") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) - ->DataElement(AZ::Edit::UIHandlers::Default, &JointLimitConfiguration::m_parentLocalRotation, - "Parent local rotation", "The rotation of the parent joint frame relative to the parent body") - ->DataElement(AZ::Edit::UIHandlers::Default, &JointLimitConfiguration::m_parentLocalPosition, - "Parent local position", "The position of the joint in the frame of the parent body") - ->DataElement(AZ::Edit::UIHandlers::Default, &JointLimitConfiguration::m_childLocalRotation, - "Child local rotation", "The rotation of the child joint frame relative to the child body") - ->DataElement(AZ::Edit::UIHandlers::Default, &JointLimitConfiguration::m_childLocalPosition, - "Child local position", "The position of the joint in the frame of the child body") - ; - } - } - } -} // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Joint.h b/Code/Framework/AzFramework/AzFramework/Physics/Joint.h deleted file mode 100644 index d78ce74611..0000000000 --- a/Code/Framework/AzFramework/AzFramework/Physics/Joint.h +++ /dev/null @@ -1,75 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#pragma once - -#include -#include - -namespace AzPhysics -{ - struct SimulatedBody; -} - -namespace Physics -{ - class JointLimitConfiguration - { - public: - AZ_CLASS_ALLOCATOR(JointLimitConfiguration, AZ::SystemAllocator, 0); - AZ_RTTI(JointLimitConfiguration, "{C9B70C4D-22D7-45AB-9B0A-30A4ED5E42DB}"); - static void Reflect(AZ::ReflectContext* context); - - JointLimitConfiguration() = default; - JointLimitConfiguration(const JointLimitConfiguration&) = default; - virtual ~JointLimitConfiguration() = default; - - virtual const char* GetTypeName(); - - AZ::Quaternion m_parentLocalRotation = AZ::Quaternion::CreateIdentity(); ///< Parent joint frame relative to parent body. - AZ::Vector3 m_parentLocalPosition = AZ::Vector3::CreateZero(); ///< Joint position relative to parent body. - AZ::Quaternion m_childLocalRotation = AZ::Quaternion::CreateIdentity(); ///< Child joint frame relative to child body. - AZ::Vector3 m_childLocalPosition = AZ::Vector3::CreateZero(); ///< Joint position relative to child body. - }; - - class Joint - { - public: - AZ_CLASS_ALLOCATOR(Joint, AZ::SystemAllocator, 0); - AZ_RTTI(Joint, "{405F517C-E986-4ACB-9606-D5D080DDE987}"); - - virtual AzPhysics::SimulatedBody* GetParentBody() const = 0; - virtual AzPhysics::SimulatedBody* GetChildBody() const = 0; - virtual void SetParentBody(AzPhysics::SimulatedBody* parentBody) = 0; - virtual void SetChildBody(AzPhysics::SimulatedBody* childBody) = 0; - virtual const AZStd::string& GetName() const = 0; - virtual void SetName(const AZStd::string& name) = 0; - virtual const AZ::Crc32 GetNativeType() const = 0; - virtual void* GetNativePointer() = 0; - /// Generates joint limit visualization data in appropriate format to pass to DebugDisplayRequests draw functions. - /// @param scale Scale factor for the output display data. - /// @param angularSubdivisions Level of detail in the angular direction (may be clamped in the implementation). - /// @param radialSubdivisions Level of detail in the radial direction (may be clamped in the implementation). - /// @param[out] vertexBufferOut Used with indexBufferOut to define triangles to be displayed. - /// @param[out] indexBufferOut Used with vertexBufferOut to define triangles to be displayed. - /// @param[out] lineBufferOut Used to define lines to be displayed. - /// @param[out] lineValidityBufferOut Whether each line in the line buffer is part of a valid or violated limit. - virtual void GenerateJointLimitVisualizationData( - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) = 0; - }; -} // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.h b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.h index 58e53b0b0d..c93278e7bf 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.h @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include namespace AzPhysics @@ -103,6 +105,26 @@ namespace AzPhysics virtual void EnableSimulationOfBody(SceneHandle sceneHandle, SimulatedBodyHandle bodyHandle) = 0; virtual void DisableSimulationOfBody(SceneHandle sceneHandle, SimulatedBodyHandle bodyHandle) = 0; + //! Add a joint to the Scene. + //! @param sceneHandle A handle to the scene to add / remove the joint. + //! @param jointConfig The config of the joint. + //! @param parentBody The parent body of the joint. + //! @param childBody The child body of the joint + //! @return Returns a handle to the created joint. Will return AzPhyiscs::InvalidJointHandle if it fails. + virtual JointHandle AddJoint(SceneHandle sceneHandle, const JointConfiguration* jointConfig, + SimulatedBodyHandle parentBody, SimulatedBodyHandle childBody) = 0; + + //! Get the Raw pointer to the requested joint. + //! @param sceneHandle A handle to the scene to get the simulated bodies from. + //! @param jointHandle A handle to the joint to retrieve the raw pointer. + //! @return A raw pointer to the Joint body. If the either handle is invalid this will return null. + virtual Joint* GetJointFromHandle(SceneHandle sceneHandle, JointHandle jointHandle) = 0; + + //! Remove a joint from the Scene. + //! @param sceneHandle A handle to the scene to add / remove the joint. + //! @param jointHandle A handle to the joint being removed. + virtual void RemoveJoint(SceneHandle sceneHandle, JointHandle jointHandle) = 0; + //! Make a blocking query into the scene. //! @param sceneHandle A handle to the scene to make the scene query with. //! @param request The request to make. Should be one of RayCastRequest || ShapeCastRequest || OverlapRequest @@ -299,6 +321,23 @@ namespace AzPhysics virtual void EnableSimulationOfBody(SimulatedBodyHandle bodyHandle) = 0; virtual void DisableSimulationOfBody(SimulatedBodyHandle bodyHandle) = 0; + //! Add a joint to the Scene. + //! @param jointConfig The config of the joint. + //! @param parentBody The parent body of the joint. + //! @param childBody The child body of the joint + //! @return Returns a handle to the created joint. Will return AzPhyiscs::InvalidJointHandle if it fails. + virtual JointHandle AddJoint(const JointConfiguration* jointConfig, + SimulatedBodyHandle parentBody, SimulatedBodyHandle childBody) = 0; + + //! Get the Raw pointer to the requested joint. + //! @param jointHandle A handle to the joint to retrieve the raw pointer. + //! @return A raw pointer to the Joint body. If the either handle is invalid this will return null. + virtual Joint* GetJointFromHandle(JointHandle jointHandle) = 0; + + //! Remove a joint from the Scene. + //! @param jointHandle A handle to the joint being removed. + virtual void RemoveJoint(JointHandle jointHandle) = 0; + //! Make a blocking query into the scene. //! @param request The request to make. Should be one of RayCastRequest || ShapeCastRequest || OverlapRequest //! @return Returns a structure that contains a list of Hits. Depending on flags set in the request, this may only contain 1 result. diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.cpp index f634360445..f4b88fe087 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.cpp @@ -36,8 +36,8 @@ namespace Physics if (serializeContext) { serializeContext->Class() - ->Version(4, &ClassConverters::RagdollNodeConfigConverter) - ->Field("JointLimit", &RagdollNodeConfiguration::m_jointLimit) + ->Version(5, &ClassConverters::RagdollNodeConfigConverter) + ->Field("JointConfig", &RagdollNodeConfiguration::m_jointConfig) ; AZ::EditContext* editContext = serializeContext->GetEditContext(); diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.h b/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.h index 97c841e8f8..5dcc93c9da 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/Ragdoll.h @@ -17,10 +17,11 @@ #include #include #include -#include #include +#include #include #include +#include namespace Physics { @@ -37,7 +38,7 @@ namespace Physics RagdollNodeConfiguration(); RagdollNodeConfiguration(const RagdollNodeConfiguration& settings) = default; - AZStd::shared_ptr m_jointLimit; + AZStd::shared_ptr m_jointConfig; }; class RagdollConfiguration @@ -73,7 +74,7 @@ namespace Physics virtual AzPhysics::RigidBody& GetRigidBody() = 0; virtual ~RagdollNode() = default; - virtual const AZStd::shared_ptr& GetJoint() const = 0; + virtual AzPhysics::Joint* GetJoint() = 0; virtual bool IsSimulating() const = 0; }; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h index 8cdd0e0cf0..73e9e4197a 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h @@ -26,23 +26,15 @@ namespace AZ namespace AzPhysics { struct SimulatedBody; - struct RigidBodyConfiguration; - struct RigidBody; } namespace Physics { - class WorldBody; class Shape; class Material; - class MaterialSelection; class MaterialConfiguration; class ColliderConfiguration; class ShapeConfiguration; - class JointLimitConfiguration; - class Joint; - class CharacterConfiguration; - class Character; /// Represents a debug vertex (position & color). struct DebugDrawVertex @@ -148,51 +140,6 @@ namespace Physics /// @param nativeMeshObject Pointer to the mesh object. virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0; - ////////////////////////////////////////////////////////////////////////// - //// Joints - - virtual AZStd::vector GetSupportedJointTypes() = 0; - virtual AZStd::shared_ptr CreateJointLimitConfiguration(AZ::TypeId jointType) = 0; - virtual AZStd::shared_ptr CreateJoint(const AZStd::shared_ptr& configuration, - AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody) = 0; - /// Generates joint limit visualization data in appropriate format to pass to DebugDisplayRequests draw functions. - /// @param configuration The joint configuration to generate visualization data for. - /// @param parentRotation The rotation of the joint's parent body (in the same frame as childRotation). - /// @param childRotation The rotation of the joint's child body (in the same frame as parentRotation). - /// @param scale Scale factor for the output display data. - /// @param angularSubdivisions Level of detail in the angular direction (may be clamped in the implementation). - /// @param radialSubdivisions Level of detail in the radial direction (may be clamped in the implementation). - /// @param[out] vertexBufferOut Used with indexBufferOut to define triangles to be displayed. - /// @param[out] indexBufferOut Used with vertexBufferOut to define triangles to be displayed. - /// @param[out] lineBufferOut Used to define lines to be displayed. - /// @param[out] lineValidityBufferOut Whether each line in the line buffer is part of a valid or violated limit. - virtual void GenerateJointLimitVisualizationData( - const JointLimitConfiguration& configuration, - const AZ::Quaternion& parentRotation, - const AZ::Quaternion& childRotation, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) = 0; - - /// Computes parameters such as joint limit local rotations to give the desired initial joint limit orientation. - /// @param jointLimitTypeId The type ID used to identify the particular kind of joint limit configuration to be created. - /// @param parentWorldRotation The rotation in world space of the parent world body associated with the joint. - /// @param childWorldRotation The rotation in world space of the child world body associated with the joint. - /// @param axis Axis used to define the centre for limiting angular degrees of freedom. - /// @param exampleLocalRotations A vector (which may be empty) containing example valid rotations in the local space - /// of the child world body relative to the parent world body, which may optionally be used to help estimate the extents - /// of the joint limit. - virtual AZStd::unique_ptr ComputeInitialJointLimitConfiguration( - const AZ::TypeId& jointLimitTypeId, - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Vector3& axis, - const AZStd::vector& exampleLocalRotations) = 0; - ////////////////////////////////////////////////////////////////////////// //// Cooking diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp index 17c09bb6ce..695fe1dcf5 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp @@ -34,6 +34,7 @@ #include #include #include +#include namespace Physics { @@ -121,9 +122,9 @@ namespace Physics DefaultMaterialConfiguration::Reflect(context); MaterialLibraryAsset::Reflect(context); MaterialInfoReflectionWrapper::Reflect(context); - JointLimitConfiguration::Reflect(context); AzPhysics::SimulatedBodyConfiguration::Reflect(context); AzPhysics::RigidBodyConfiguration::Reflect(context); + AzPhysics::JointConfiguration::Reflect(context); RagdollNodeConfiguration::Reflect(context); RagdollConfiguration::Reflect(context); CharacterColliderNodeConfiguration::Reflect(context); @@ -131,6 +132,7 @@ namespace Physics AnimationConfiguration::Reflect(context); CharacterConfiguration::Reflect(context); AzPhysics::SimulatedBody::Reflect(context); + AzPhysics::Joint::Reflect(context); ReflectSimulatedBodyComponentRequestsBus(context); CollisionFilteringRequests::Reflect(context); AzPhysics::SceneQuery::ReflectSceneQueryObjects(context); diff --git a/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp b/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp index 4f8da821c1..d80094a071 100644 --- a/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp @@ -693,11 +693,11 @@ namespace AzFramework // set the __index so we can read values in case we change the script // after we export the component lua_pushliteral(lua, "__index"); - lua_pushcclosure(lua, &Internal::Properties__Index, 1); + lua_pushcclosure(lua, &Internal::Properties__Index, 0); lua_rawset(lua, -3); lua_pushliteral(lua, "__newindex"); - lua_pushcclosure(lua, &Internal::Properties__NewIndex, 1); + lua_pushcclosure(lua, &Internal::Properties__NewIndex, 0); lua_rawset(lua, -3); } lua_pop(lua, 1); // pop the properties table (or the nil value) @@ -900,11 +900,11 @@ namespace AzFramework // Ensure that this instance of Properties table has the proper __index and __newIndex metamethods. lua_newtable(lua); // This new table will become the Properties instance metatable. Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {} lua_pushliteral(lua, "__index"); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {} __index - lua_pushcclosure(lua, &Internal::Properties__Index, 1); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {} __index function + lua_pushcclosure(lua, &Internal::Properties__Index, 0); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {} __index function lua_rawset(lua, -3); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {__index=Internal::Properties__Index} lua_pushliteral(lua, "__newindex"); - lua_pushcclosure(lua, &Internal::Properties__NewIndex, 1); + lua_pushcclosure(lua, &Internal::Properties__NewIndex, 0); lua_rawset(lua, -3); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {} {__index=Internal::Properties__Index __newindex=Internal::Properties__NewIndex} lua_setmetatable(lua, -2); // Stack: ScriptRootTable PropertiesTable EntityTable "Properties" {Meta{__index=Internal::Properties__Index __newindex=Internal::Properties__NewIndex} } diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index 13dff43f68..2a5d251400 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -204,6 +204,8 @@ set(FILES Physics/Collision/CollisionLayers.cpp Physics/Collision/CollisionGroups.h Physics/Collision/CollisionGroups.cpp + Physics/Common/PhysicsJoint.h + Physics/Common/PhysicsJoint.cpp Physics/Common/PhysicsSceneQueries.h Physics/Common/PhysicsSceneQueries.cpp Physics/Common/PhysicsEvents.h @@ -215,6 +217,8 @@ set(FILES Physics/Common/PhysicsSimulatedBodyEvents.cpp Physics/Common/PhysicsTypes.h Physics/Components/SimulatedBodyComponentBus.h + Physics/Configuration/JointConfiguration.h + Physics/Configuration/JointConfiguration.cpp Physics/Configuration/CollisionConfiguration.h Physics/Configuration/CollisionConfiguration.cpp Physics/Configuration/RigidBodyConfiguration.h @@ -259,8 +263,6 @@ set(FILES Physics/Ragdoll.h Physics/Utils.h Physics/Utils.cpp - Physics/Joint.h - Physics/Joint.cpp Physics/ClassConverters.cpp Physics/ClassConverters.h Physics/MaterialBus.h diff --git a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h index b61956bdf9..d0107bb317 100644 --- a/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h +++ b/Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h @@ -13,6 +13,7 @@ #pragma once #include +#include #include #include #include diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp index 0f58f06420..47c0e66ff0 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp @@ -493,6 +493,29 @@ namespace AzQtComponents } } break; + case CE_MenuItem: + { + const QMenu* menu = qobject_cast(widget); + QAction* action = menu->activeAction(); + if (action) + { + QMenu* subMenu = action->menu(); + if (subMenu) + { + QVariant noHover = subMenu->property("noHover"); + if (noHover.isValid() && noHover.toBool()) + { + // First draw as standard to get the correct hover background for the complete control. + QProxyStyle::drawControl(element, option, painter, widget); + // Now draw the icon as non-hovered so control behaves as designed. + QStyleOptionMenuItem myOpt = *qstyleoption_cast(option); + myOpt.state &= ~QStyle::State_Selected; + return QProxyStyle::drawControl(element, &myOpt, painter, widget); + } + } + } + } + break; } return QProxyStyle::drawControl(element, option, painter, widget); diff --git a/Assets/Editor/Icons/WhiteBox/RestoreMode.svg b/Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/RestoreMode.svg similarity index 100% rename from Assets/Editor/Icons/WhiteBox/RestoreMode.svg rename to Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/RestoreMode.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Translate.svg b/Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Rotate.svg similarity index 100% rename from Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Translate.svg rename to Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/Rotate.svg diff --git a/Assets/Editor/Icons/WhiteBox/SketchMode.svg b/Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/SketchMode.svg similarity index 100% rename from Assets/Editor/Icons/WhiteBox/SketchMode.svg rename to Code/Framework/AzQtComponents/AzQtComponents/Components/img/UI20/toolbar/SketchMode.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc b/Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc index 7070bd372b..d6cd99b1d9 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/resources.qrc @@ -370,6 +370,7 @@ img/UI20/toolbar/Redo.svg img/UI20/toolbar/remove_link.svg img/UI20/toolbar/Reset_physics_state.svg + img/UI20/toolbar/RestoreMode.svg img/UI20/toolbar/Save.svg img/UI20/toolbar/Scale.svg img/UI20/toolbar/Select.svg @@ -377,9 +378,10 @@ img/UI20/toolbar/Select_terrain.svg img/UI20/toolbar/Simulate_Physics.svg img/UI20/toolbar/Simulate_Physics_on_selected_objects.svg + img/UI20/toolbar/SketchMode.svg img/UI20/toolbar/Terrain.svg img/UI20/toolbar/Terrain_Texture.svg - img/UI20/toolbar/Translate.svg + img/UI20/toolbar/Rotate.svg img/UI20/toolbar/undo.svg img/UI20/toolbar/Unlocked.svg img/UI20/toolbar/Vertex_snapping.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg index dfd21d157f..6f5608c092 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg @@ -1,4 +1,4 @@ - - + + diff --git a/Code/Framework/AzTest/CMakeLists.txt b/Code/Framework/AzTest/CMakeLists.txt index fe5ec2d0ff..d7b8813639 100644 --- a/Code/Framework/AzTest/CMakeLists.txt +++ b/Code/Framework/AzTest/CMakeLists.txt @@ -8,25 +8,27 @@ # 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. # - -ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/AzTest/Platform/${PAL_PLATFORM_NAME}) -ly_add_target( - NAME AzTest STATIC - NAMESPACE AZ - FILES_CMAKE - AzTest/aztest_files.cmake - ${pal_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake - INCLUDE_DIRECTORIES - PUBLIC - . - ${pal_dir} - BUILD_DEPENDENCIES - PUBLIC - 3rdParty::googletest::GMock - 3rdParty::googletest::GTest - 3rdParty::GoogleBenchmark - AZ::AzCore - PLATFORM_INCLUDE_FILES +if(NOT LY_MONOLITHIC_GAME) + ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/AzTest/Platform/${PAL_PLATFORM_NAME}) + + ly_add_target( + NAME AzTest STATIC + NAMESPACE AZ + FILES_CMAKE + AzTest/aztest_files.cmake + ${pal_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + . + ${pal_dir} + BUILD_DEPENDENCIES + PUBLIC + 3rdParty::googletest::GMock + 3rdParty::googletest::GTest + 3rdParty::GoogleBenchmark + AZ::AzCore + PLATFORM_INCLUDE_FILES ${pal_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake -) + ) +endif() diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp index 9c60b6cd35..adb4ae66c4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp @@ -12,6 +12,7 @@ #include #include #include +#include AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") #include @@ -22,6 +23,9 @@ AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") #include AZ_POP_DISABLE_WARNING +AZ_CVAR( + bool, ed_useNewAssetBrowserTableView, false, nullptr, AZ::ConsoleFunctorFlags::Null, + "Use the new AssetBrowser TableView for searching assets."); namespace AzToolsFramework { namespace AssetBrowser @@ -31,7 +35,11 @@ namespace AzToolsFramework AssetBrowserFilterModel::AssetBrowserFilterModel(QObject* parent) : QSortFilterProxyModel(parent) { - m_showColumn.insert(AssetBrowserModel::m_column); + m_showColumn.insert(aznumeric_cast(AssetBrowserEntry::Column::DisplayName)); + if (ed_useNewAssetBrowserTableView) + { + m_showColumn.insert(aznumeric_cast(AssetBrowserEntry::Column::Path)); + } m_collator.setNumericMode(true); AssetBrowserComponentNotificationBus::Handler::BusConnect(); } @@ -128,28 +136,58 @@ namespace AzToolsFramework auto compFilter = qobject_cast >(m_filter); if (compFilter) { - auto& subFilters = compFilter->GetSubFilters(); - auto it = AZStd::find_if(subFilters.begin(), subFilters.end(), [subFilters](FilterConstType filter) -> bool + const auto& subFilters = compFilter->GetSubFilters(); + + const auto compositeFilterIterator = AZStd::find_if(subFilters.cbegin(), subFilters.cend(), [subFilters](FilterConstType filter) -> bool { - auto assetTypeFilter = qobject_cast >(filter); + const auto assetTypeFilter = qobject_cast >(filter); return !assetTypeFilter.isNull(); }); - if (it != subFilters.end()) + + if (compositeFilterIterator != subFilters.end()) { - m_assetTypeFilter = qobject_cast >(*it); + m_assetTypeFilter = qobject_cast >(*compositeFilterIterator); } - it = AZStd::find_if(subFilters.begin(), subFilters.end(), [subFilters](FilterConstType filter) -> bool + + const auto compStringFilterIter = AZStd::find_if(subFilters.cbegin(), subFilters.cend(), [](FilterConstType filter) -> bool { - auto stringFilter = qobject_cast >(filter); - return !stringFilter.isNull(); + //The real StringFilter is really a CompositeFilter with just one StringFilter in its subfilter list + //To know if it is actually a StringFilter we have to get that subfilter and check if it is a Stringfilter. + const auto stringCompositeFilter = qobject_cast >(filter); + bool isStringFilter = false; + if (stringCompositeFilter) + { + const auto& stringSubfilters = stringCompositeFilter->GetSubFilters(); + auto canBeCasted = [](FilterConstType filt) -> bool + { + auto strFilter = qobject_cast>(filt); + return !strFilter.isNull(); + }; + const auto stringSubfliterConstIter = AZStd::find_if(stringSubfilters.cbegin(), stringSubfilters.cend(), canBeCasted); + + //A Composite StringFilter will only have just one subfilter and nothing more. + if (stringSubfliterConstIter != stringSubfilters.end() && stringSubfilters.size() == 1) + { + isStringFilter = true; + } + } + + return isStringFilter; }); - if (it != subFilters.end()) + if (compStringFilterIter != subFilters.end()) { - m_stringFilter = qobject_cast >(*it); + const auto compStringFilter = qobject_cast>(*compStringFilterIter); + + if (!compStringFilter->GetSubFilters().isEmpty() && compStringFilter->GetSubFilters()[0]) + { + m_stringFilter = qobject_cast>(compStringFilter->GetSubFilters()[0]); + } } + } invalidateFilter(); Q_EMIT filterChanged(); + emit stringFilterPopulated(!m_stringFilter.isNull()); } void AssetBrowserFilterModel::filterUpdatedSlot() diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.h index f9423b2534..5d22791699 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.h @@ -45,15 +45,15 @@ namespace AzToolsFramework //asset type filtering void SetFilter(FilterConstType filter); void FilterUpdatedSlotImmediate(); - + const FilterConstType& GetFilter() const { return m_filter; } ////////////////////////////////////////////////////////////////////////// // AssetBrowserComponentNotificationBus ////////////////////////////////////////////////////////////////////////// void OnAssetBrowserComponentReady() override; Q_SIGNALS: + void stringFilterPopulated(bool); void filterChanged(); - ////////////////////////////////////////////////////////////////////////// //QSortFilterProxyModel protected: @@ -68,7 +68,7 @@ namespace AzToolsFramework protected: //set for filtering columns //if the column is in the set the column is not filtered and is shown - AZStd::fixed_unordered_set(AssetBrowserEntry::Column::Count)> m_showColumn; + AZStd::fixed_unordered_set(AssetBrowserEntry::Column::Count)> m_showColumn; bool m_alreadyRecomputingFilters = false; //asset source name match filter FilterConstType m_filter; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.cpp index d7c0164435..c0f78baccb 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.cpp @@ -27,8 +27,6 @@ namespace AzToolsFramework { namespace AssetBrowser { - const int AssetBrowserModel::m_column = static_cast(AssetBrowserEntry::Column::DisplayName); - AssetBrowserModel::AssetBrowserModel(QObject* parent) : QAbstractItemModel(parent) , m_rootEntry(nullptr) @@ -143,8 +141,9 @@ namespace AzToolsFramework if (parent.isValid()) { - if ((parent.column() != static_cast(AssetBrowserEntry::Column::DisplayName)) && - (parent.column() != static_cast(AssetBrowserEntry::Column::Name))) + if ((parent.column() != aznumeric_cast(AssetBrowserEntry::Column::DisplayName)) && + (parent.column() != aznumeric_cast(AssetBrowserEntry::Column::Name)) && + (parent.column() != aznumeric_cast(AssetBrowserEntry::Column::Path))) { return 0; } @@ -164,7 +163,7 @@ namespace AzToolsFramework int AssetBrowserModel::columnCount(const QModelIndex& /*parent*/) const { - return static_cast(AssetBrowserEntry::Column::Count); + return aznumeric_cast(AssetBrowserEntry::Column::Count); } QVariant AssetBrowserModel::data(const QModelIndex& index, int role) const @@ -393,7 +392,7 @@ namespace AzToolsFramework } int row = entry->row(); - index = createIndex(row, m_column, entry); + index = createIndex(row, aznumeric_cast(AssetBrowserEntry::Column::DisplayName), entry); return true; } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.h index 9e60c44dae..3c4cae5588 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserModel.h @@ -91,8 +91,6 @@ namespace AzToolsFramework static void SourceIndexesToAssetIds(const QModelIndexList& indexes, AZStd::vector& assetIds); static void SourceIndexesToAssetDatabaseEntries(const QModelIndexList& indexes, AZStd::vector& entries); - const static int m_column; - private: AZStd::shared_ptr m_rootEntry; bool m_loaded; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp new file mode 100644 index 0000000000..d22486d481 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp @@ -0,0 +1,136 @@ +/* + * 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 AzToolsFramework +{ + namespace AssetBrowser + { + AssetBrowserTableModel::AssetBrowserTableModel(QObject* parent /* = nullptr */) + : QSortFilterProxyModel(parent) + { + setDynamicSortFilter(false); + } + + void AssetBrowserTableModel::setSourceModel(QAbstractItemModel* sourceModel) + { + m_filterModel = qobject_cast(sourceModel); + AZ_Assert( + m_filterModel, + "Error in AssetBrowserTableModel initialization, class expects source model to be an AssetBrowserFilterModel."); + QSortFilterProxyModel::setSourceModel(sourceModel); + } + + QModelIndex AssetBrowserTableModel::mapToSource(const QModelIndex& proxyIndex) const + { + Q_ASSERT(!proxyIndex.isValid() || proxyIndex.model() == this); + if (!proxyIndex.isValid()) + { + return QModelIndex(); + } + return m_indexMap[proxyIndex.row()]; + } + + QVariant AssetBrowserTableModel::headerData(int section, Qt::Orientation orientation, int role) const + { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) + { + return tr(AssetBrowserEntry::m_columnNames[section]); + } + return QSortFilterProxyModel::headerData(section, orientation, role); + } + + QVariant AssetBrowserTableModel::data(const QModelIndex& index, int role) const + { + auto sourceIndex = mapToSource(index); + if (!sourceIndex.isValid()) + { + return QVariant(); + } + + AssetBrowserEntry* entry = GetAssetEntry(sourceIndex); + if (entry == nullptr) + { + AZ_Assert(false, "AssetBrowserTableModel - QModelIndex does not reference an AssetEntry. Source model is not valid."); + return QVariant(); + } + + return sourceIndex.data(role); + } + + QModelIndex AssetBrowserTableModel::index(int row, int column, const QModelIndex& parent) const + { + return parent.isValid() ? QModelIndex() : createIndex(row, column, m_indexMap[row].internalPointer()); + } + + int AssetBrowserTableModel::rowCount(const QModelIndex& parent) const + { + return !parent.isValid() ? m_indexMap.size() : 0; + } + + int AssetBrowserTableModel::BuildTableModelMap( + const QAbstractItemModel* model, const QModelIndex& parent /*= QModelIndex()*/, int row /*= 0*/) + { + int rows = model ? model->rowCount(parent) : 0; + for (int i = 0; i < rows; ++i) + { + QModelIndex index = model->index(i, 0, parent); + AssetBrowserEntry* entry = GetAssetEntry(m_filterModel->mapToSource(index)); + //We only wanna see the source assets. + if (entry->GetEntryType() == AssetBrowserEntry::AssetEntryType::Source) + { + beginInsertRows(parent, row, row); + m_indexMap[row] = index; + endInsertRows(); + + Q_EMIT dataChanged(index, index); + ++row; + } + + if (model->hasChildren(index)) + { + row = BuildTableModelMap(model, index, row); + } + } + return row; + } + + AssetBrowserEntry* AssetBrowserTableModel::GetAssetEntry(QModelIndex index) const + { + if (index.isValid()) + { + return static_cast(index.internalPointer()); + } + else + { + AZ_Error("AssetBrowser", false, "Invalid Source Index provided to GetAssetEntry."); + return nullptr; + } + } + + void AssetBrowserTableModel::UpdateTableModelMaps() + { + emit layoutAboutToBeChanged(); + if (!m_indexMap.isEmpty()) + { + beginRemoveRows(m_indexMap.first(), m_indexMap.first().row(), m_indexMap.last().row()); + m_indexMap.clear(); + endRemoveRows(); + } + BuildTableModelMap(sourceModel()); + emit layoutChanged(); + } + } // namespace AssetBrowser +} // namespace AzToolsFramework +#include "AssetBrowser/moc_AssetBrowserTableModel.cpp" diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.h new file mode 100644 index 0000000000..e438bf6271 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.h @@ -0,0 +1,59 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#include +#endif + +namespace AzToolsFramework +{ + namespace AssetBrowser + { + class AssetBrowserFilterModel; + class AssetBrowserEntry; + + class AssetBrowserTableModel + : public QSortFilterProxyModel + { + Q_OBJECT + + public: + AZ_CLASS_ALLOCATOR(AssetBrowserTableModel, AZ::SystemAllocator, 0); + explicit AssetBrowserTableModel(QObject* parent = nullptr); + //////////////////////////////////////////////////////////////////// + // QSortFilterProxyModel + void setSourceModel(QAbstractItemModel* sourceModel) override; + QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + public Q_SLOTS: + void UpdateTableModelMaps(); + + protected: + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role /* = Qt::DisplayRole */) const override; + //////////////////////////////////////////////////////////////////// + + private: + AssetBrowserEntry* GetAssetEntry(QModelIndex index) const; + int BuildTableModelMap(const QAbstractItemModel* model, const QModelIndex& parent = QModelIndex(), int row = 0); + + private: + QPointer m_filterModel; + QMap m_indexMap; + }; + } // namespace AssetBrowser +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.cpp index abd1f91bf5..8ee8f44137 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.cpp @@ -46,6 +46,7 @@ namespace AzToolsFramework const char* AssetBrowserEntry::m_columnNames[] = { "Name", + "Path", "Source ID", "Fingerprint", "Guid", @@ -128,6 +129,8 @@ namespace AzToolsFramework return QString::fromUtf8(m_name.c_str()); case Column::DisplayName: return m_displayName; + case Column::Path: + return m_displayPath; default: return QVariant(); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h index 6f0eceeeea..ac5470a136 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h @@ -68,6 +68,7 @@ namespace AzToolsFramework enum class Column { Name, + Path, SourceID, Fingerprint, Guid, @@ -135,6 +136,7 @@ namespace AzToolsFramework protected: AZStd::string m_name; QString m_displayName; + QString m_displayPath; AZStd::string m_relativePath; AZStd::string m_fullPath; AZStd::vector m_children; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.cpp index 88b26ad5ee..0312edbc51 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.cpp @@ -43,6 +43,7 @@ namespace AzToolsFramework void FolderAssetBrowserEntry::UpdateChildPaths(AssetBrowserEntry* child) const { child->m_relativePath = m_relativePath + AZ_CORRECT_DATABASE_SEPARATOR + child->m_name; + child->m_displayPath = QString::fromUtf8(child->m_relativePath.c_str()); child->m_fullPath = m_fullPath + AZ_CORRECT_DATABASE_SEPARATOR + child->m_name; AssetBrowserEntry::UpdateChildPaths(child); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.cpp index bd5ba48d8d..e626f31b62 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.cpp @@ -286,6 +286,9 @@ namespace AzToolsFramework product->m_assetType = productWithUuidDatabaseEntry.second.m_assetType; product->m_assetType.ToString(product->m_assetTypeString); AZ::Data::AssetCatalogRequestBus::BroadcastResult(product->m_relativePath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, assetId); + QString displayPath = QString::fromUtf8(product->m_relativePath.c_str()); + displayPath.remove(QString(AZ_CORRECT_DATABASE_SEPARATOR + QString::fromUtf8(product->m_name.c_str()))); + product->m_displayPath = displayPath; EntryCache::GetInstance()->m_productAssetIdMap[assetId] = product; if (needsAdd) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.cpp new file mode 100644 index 0000000000..894bbd8700 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.cpp @@ -0,0 +1,178 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +AZ_PUSH_DISABLE_WARNING( + 4244 4251 4800, "-Wunknown-warning-option") // conversion from 'int' to 'float', possible loss of data, needs to have dll-interface to + // be used by clients of class 'QFlags::Int': forcing value to bool + // 'true' or 'false' (performance warning) +#include +#include +#include +#include +#include +#include +#include +AZ_POP_DISABLE_WARNING +namespace AzToolsFramework +{ + namespace AssetBrowser + { + AssetBrowserTableView::AssetBrowserTableView(QWidget* parent) + : QTableView(parent) + , m_delegate(new EntryDelegate(this)) + { + setSortingEnabled(true); + setItemDelegate(m_delegate); + verticalHeader()->hide(); + setContextMenuPolicy(Qt::CustomContextMenu); + + setMouseTracking(true); + setSortingEnabled(false); + setSelectionMode(QAbstractItemView::SingleSelection); + + connect(this, &QTableView::customContextMenuRequested, this, &AssetBrowserTableView::OnContextMenu); + + AssetBrowserViewRequestBus::Handler::BusConnect(); + AssetBrowserComponentNotificationBus::Handler::BusConnect(); + } + + AssetBrowserTableView::~AssetBrowserTableView() + { + AssetBrowserViewRequestBus::Handler::BusDisconnect(); + AssetBrowserComponentNotificationBus::Handler::BusDisconnect(); + } + + void AssetBrowserTableView::setModel(QAbstractItemModel* model) + { + m_tableModel = qobject_cast(model); + AZ_Assert(m_tableModel, "Expecting AssetBrowserTableModel"); + m_sourceFilterModel = qobject_cast(m_tableModel->sourceModel()); + QTableView::setModel(model); + connect(m_tableModel, &AssetBrowserTableModel::layoutChanged, this, &AssetBrowserTableView::layoutChangedSlot); + + horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeMode::Stretch); + horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Stretch); + } + + void AssetBrowserTableView::SetName(const QString& name) + { + m_name = name; + bool isAssetBrowserComponentReady = false; + AssetBrowserComponentRequestBus::BroadcastResult(isAssetBrowserComponentReady, &AssetBrowserComponentRequests::AreEntriesReady); + if (isAssetBrowserComponentReady) + { + OnAssetBrowserComponentReady(); + } + } + + AZStd::vector AssetBrowserTableView::GetSelectedAssets() const + { + QModelIndexList sourceIndexes; + for (const auto& index : selectedIndexes()) + { + if (index.column() == 0) + { + sourceIndexes.push_back(m_sourceFilterModel->mapToSource(m_tableModel->mapToSource(index))); + } + } + + AZStd::vector entries; + AssetBrowserModel::SourceIndexesToAssetDatabaseEntries(sourceIndexes, entries); + return entries; + } + + void AssetBrowserTableView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) + { + QTableView::selectionChanged(selected, deselected); + Q_EMIT selectionChangedSignal(selected, deselected); + } + + void AssetBrowserTableView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) + { + // if selected entry is being removed, clear selection so not to select (and attempt to preview) other entries potentially + // marked for deletion + if (selectionModel() && selectionModel()->selectedIndexes().size() == 1) + { + QModelIndex selectedIndex = selectionModel()->selectedIndexes().first(); + QModelIndex parentSelectedIndex = selectedIndex.parent(); + if (parentSelectedIndex == parent && selectedIndex.row() >= start && selectedIndex.row() <= end) + { + selectionModel()->clear(); + } + } + QTableView::rowsAboutToBeRemoved(parent, start, end); + } + + void AssetBrowserTableView::layoutChangedSlot( + [[maybe_unused]] const QList& parents, [[maybe_unused]] QAbstractItemModel::LayoutChangeHint hint) + { + scrollToTop(); + } + + void AssetBrowserTableView::SelectProduct([[maybe_unused]] AZ::Data::AssetId assetID) + { + } + + void AssetBrowserTableView::SelectFileAtPath([[maybe_unused]] const AZStd::string& assetPath) + { + } + + void AssetBrowserTableView::ClearFilter() + { + emit ClearStringFilter(); + emit ClearTypeFilter(); + m_sourceFilterModel->FilterUpdatedSlotImmediate(); + } + + void AssetBrowserTableView::Update() + { + update(); + } + + void AssetBrowserTableView::OnAssetBrowserComponentReady() + { + } + + void AssetBrowserTableView::OnContextMenu([[maybe_unused]] const QPoint& point) + { + const auto& selectedAssets = GetSelectedAssets(); + if (selectedAssets.size() != 1) + { + return; + } + + QMenu menu(this); + AssetBrowserInteractionNotificationBus::Broadcast( + &AssetBrowserInteractionNotificationBus::Events::AddContextMenuActions, this, &menu, selectedAssets); + if (!menu.isEmpty()) + { + menu.exec(QCursor::pos()); + } + } + } // namespace AssetBrowser +} // namespace AzToolsFramework +#include "AssetBrowser/Views/moc_AssetBrowserTableView.cpp" diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.h new file mode 100644 index 0000000000..15bc95f13c --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.h @@ -0,0 +1,84 @@ +/* + * 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 +#if !defined(Q_MOC_RUN) +#include +#include +#include + +#include +#include + +#include +#include +#include +#endif + +namespace AzToolsFramework +{ + namespace AssetBrowser + { + class AssetBrowserEntry; + class AssetBrowserTableModel; + class AssetBrowserFilterModel; + class EntryDelegate; + + class AssetBrowserTableView //! Table view that displays the asset browser entries in a list. + : public QTableView + , public AssetBrowserViewRequestBus::Handler + , public AssetBrowserComponentNotificationBus::Handler + { + Q_OBJECT + public: + explicit AssetBrowserTableView(QWidget* parent = nullptr); + ~AssetBrowserTableView() override; + + void setModel(QAbstractItemModel *model) override; + void SetName(const QString& name); + + AZStd::vector GetSelectedAssets() const; + + ////////////////////////////////////////////////////////////////////////// + // AssetBrowserViewRequestBus + virtual void SelectProduct(AZ::Data::AssetId assetID) override; + virtual void SelectFileAtPath(const AZStd::string& assetPath) override; + virtual void ClearFilter() override; + virtual void Update() override; + + ////////////////////////////////////////////////////////////////////////// + // AssetBrowserComponentNotificationBus + void OnAssetBrowserComponentReady() override; + ////////////////////////////////////////////////////////////////////////// + + + Q_SIGNALS: + void selectionChangedSignal(const QItemSelection& selected, const QItemSelection& deselected); + void ClearStringFilter(); + void ClearTypeFilter(); + + protected Q_SLOTS: + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; + void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; + void layoutChangedSlot(const QList &parents = QList(), + QAbstractItemModel::LayoutChangeHint hint = QAbstractItemModel::NoLayoutChangeHint); + + private: + QString m_name; + QPointer m_tableModel = nullptr; + QPointer m_sourceFilterModel = nullptr; + EntryDelegate* m_delegate = nullptr; + + private Q_SLOTS: + void OnContextMenu(const QPoint& point); + }; + } // namespace AssetBrowser +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp index 6cd60b0015..026343c2a4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/AssetBrowserTreeView.cpp @@ -53,6 +53,7 @@ namespace AzToolsFramework setSortingEnabled(true); setItemDelegate(m_delegate); header()->hide(); + setContextMenuPolicy(Qt::CustomContextMenu); setMouseTracking(true); @@ -99,8 +100,9 @@ namespace AzToolsFramework AZStd::vector AssetBrowserTreeView::GetSelectedAssets() const { + const QModelIndexList& selectedIndexes = selectionModel()->selectedRows(); QModelIndexList sourceIndexes; - for (const auto& index : selectedIndexes()) + for (const auto& index : selectedIndexes) { sourceIndexes.push_back(m_assetBrowserSortFilterProxyModel->mapToSource(index)); } @@ -172,6 +174,7 @@ namespace AzToolsFramework void AssetBrowserTreeView::OnAssetBrowserComponentReady() { + hideColumn(aznumeric_cast(AssetBrowserEntry::Column::Path)); if (!m_name.isEmpty()) { auto crc = AZ::Crc32(m_name.toUtf8().data()); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp index e8abaebd6f..d0ac05020d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/Views/EntryDelegate.cpp @@ -74,34 +74,34 @@ namespace AzToolsFramework QPoint iconTopLeft(remainingRect.x(), remainingRect.y() + (remainingRect.height() / 2) - (m_iconSize / 2)); auto sourceEntry = azrtti_cast(entry); - - int thumbX = DrawThumbnail(painter, iconTopLeft, iconSize, entry->GetThumbnailKey()); - QPalette actualPalette(option.palette); - - if (sourceEntry) + if (index.column() == aznumeric_cast(AssetBrowserEntry::Column::Name)) { - if (m_showSourceControl) + int thumbX = DrawThumbnail(painter, iconTopLeft, iconSize, entry->GetThumbnailKey()); + if (sourceEntry) { - DrawThumbnail(painter, iconTopLeft, iconSize, sourceEntry->GetSourceControlThumbnailKey()); + if (m_showSourceControl) + { + DrawThumbnail(painter, iconTopLeft, iconSize, sourceEntry->GetSourceControlThumbnailKey()); + } + // sources with no children should be greyed out. + if (sourceEntry->GetChildCount() == 0) + { + isEnabled = false; // draw in disabled style. + actualPalette.setCurrentColorGroup(QPalette::Disabled); + } } - // sources with no children should be greyed out. - if (sourceEntry->GetChildCount() == 0) - { - isEnabled = false; // draw in disabled style. - actualPalette.setCurrentColorGroup(QPalette::Disabled); - } - } - remainingRect.adjust(thumbX, 0, 0, 0); // bump it to the right by the size of the thumbnail - remainingRect.adjust(ENTRY_SPACING_LEFT_PIXELS, 0, 0, 0); // bump it to the right by the spacing. + remainingRect.adjust(thumbX, 0, 0, 0); // bump it to the right by the size of the thumbnail + remainingRect.adjust(ENTRY_SPACING_LEFT_PIXELS, 0, 0, 0); // bump it to the right by the spacing. + } + QString displayString = index.column() == aznumeric_cast(AssetBrowserEntry::Column::Name) + ? qvariant_cast(entry->data(aznumeric_cast(AssetBrowserEntry::Column::Name))) + : qvariant_cast(entry->data(aznumeric_cast(AssetBrowserEntry::Column::Path))); - style->drawItemText(painter, - remainingRect, - option.displayAlignment, - actualPalette, - isEnabled, - entry->GetDisplayName(), + style->drawItemText( + painter, remainingRect, option.displayAlignment, actualPalette, isEnabled, + displayString, isSelected ? QPalette::HighlightedText : QPalette::Text); } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index fa7d5f6e9b..336b56653b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -159,12 +159,23 @@ namespace AzToolsFramework void PrefabEditorEntityOwnershipService::GetNonPrefabEntities(EntityList& entities) { - m_rootInstance->GetEntities(entities, false); + m_rootInstance->GetEntities( + [&entities](const AZStd::unique_ptr& entity) + { + entities.emplace_back(entity.get()); + return true; + }); } bool PrefabEditorEntityOwnershipService::GetAllEntities(EntityList& entities) { - m_rootInstance->GetEntities(entities, true); + m_rootInstance->GetAllEntitiesInHierarchy( + [&entities](const AZStd::unique_ptr& entity) + { + entities.emplace_back(entity.get()); + return true; + }); + return true; } @@ -252,13 +263,20 @@ namespace AzToolsFramework } AZStd::string out; - if (m_loaderInterface->SaveTemplateToString(m_rootInstance->GetTemplateId(), out)) + + if (!m_loaderInterface->SaveTemplateToString(m_rootInstance->GetTemplateId(), out)) { - const size_t bytesToWrite = out.size(); - const size_t bytesWritten = stream.Write(bytesToWrite, out.data()); - return bytesWritten == bytesToWrite; + return false; } - return false; + + const size_t bytesToWrite = out.size(); + const size_t bytesWritten = stream.Write(bytesToWrite, out.data()); + if(bytesWritten != bytesToWrite) + { + return false; + } + m_prefabSystemComponent->SetTemplateDirtyFlag(templateId, false); + return true; } void PrefabEditorEntityOwnershipService::CreateNewLevelPrefab(AZStd::string_view filename, const AZStd::string& templateFilename) @@ -544,7 +562,7 @@ namespace AzToolsFramework return; } - m_rootInstance->GetNestedEntities([this](AZStd::unique_ptr& entity) + m_rootInstance->GetAllEntitiesInHierarchy([this](AZStd::unique_ptr& entity) { AZ_Assert(entity, "Invalid entity found in root instance while starting play in editor."); if (entity->GetState() == AZ::Entity::State::Active) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.cpp index 427bb9e9d1..27d8f7e5a3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -95,16 +96,34 @@ namespace AzToolsFramework return axis * snapAdjustment.m_nextSnapDistance; } + AZ::Vector3 CalculateSnappedOffset( + const AZ::Vector3& unsnappedPosition, const AZ::Vector3* snapAxes, const size_t snapAxesCount, const float size) + { + return AZStd::accumulate( + snapAxes, snapAxes + snapAxesCount, AZ::Vector3::CreateZero(), + [&unsnappedPosition, size](AZ::Vector3 acc, const AZ::Vector3& snapAxis) + { + acc += CalculateSnappedOffset(unsnappedPosition, snapAxis, size); + return acc; + }); + } + + AZ::Vector3 CalculateSnappedPosition( + const AZ::Vector3& unsnappedPosition, const AZ::Vector3* snapAxes, const size_t snapAxesCount, const float size) + { + return unsnappedPosition + CalculateSnappedOffset(unsnappedPosition, snapAxes, snapAxesCount, size); + } + AZ::Vector3 CalculateSnappedTerrainPosition( - const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, const int viewportId, const float gridSize) + const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, const int viewportId, const float size) { const AZ::Transform localFromWorld = worldFromLocal.GetInverse(); const AZ::Vector3 localSurfacePosition = localFromWorld.TransformPoint(worldSurfacePosition); // snap in xy plane AZ::Vector3 localSnappedSurfacePosition = localSurfacePosition + - CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisX(), gridSize) + - CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisY(), gridSize); + CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisX(), size) + + CalculateSnappedOffset(localSurfacePosition, AZ::Vector3::CreateAxisY(), size); // find terrain height at xy snapped location float terrainHeight = 0.0f; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.h index f2fa104d4c..9024625fae 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Manipulators/ManipulatorSnapping.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include @@ -58,10 +59,18 @@ namespace AzToolsFramework //! @note A movement of more than half size (in either direction) will cause a snap by size. AZ::Vector3 CalculateSnappedAmount(const AZ::Vector3& unsnappedPosition, const AZ::Vector3& axis, float size); + //! Overload of CalculateSnappedOffset taking multiple axes. + AZ::Vector3 CalculateSnappedOffset( + const AZ::Vector3& unsnappedPosition, const AZ::Vector3* snapAxes, size_t snapAxesCount, float size); + + //! Return the final snapped position according to size (unsnappedPosition + CalculateSnappedOffset). + AZ::Vector3 CalculateSnappedPosition( + const AZ::Vector3& unsnappedPosition, const AZ::Vector3* snapAxes, size_t snapAxesCount, float size); + //! For a given point on the terrain, calculate the closest xy position snapped to the grid //! (z position is aligned to terrain height, not snapped to z grid) AZ::Vector3 CalculateSnappedTerrainPosition( - const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, int viewportId, float gridSize); + const AZ::Vector3& worldSurfacePosition, const AZ::Transform& worldFromLocal, int viewportId, float size); //! Wrapper for grid snapping and grid size bus calls. GridSnapParameters GridSnapSettings(int viewportId); @@ -84,8 +93,8 @@ namespace AzToolsFramework //! @param exponent Precision to use when rounding. inline float Round(const float value, const float exponent) { - const float precision = std::pow(10.0f, exponent); - return roundf(value * precision) / precision; + const float precision = AZStd::pow(10.0f, exponent); + return AZStd::round(value * precision) / precision; } //! Round to 3 significant digits (3 digits common usage). @@ -116,7 +125,7 @@ namespace AzToolsFramework //! when dealing with values far from the origin. inline AZ::Vector3 NonUniformScaleReciprocal(const AZ::Vector3& nonUniformScale) { - AZ::Vector3 scaleReciprocal = nonUniformScale.GetReciprocal(); + const AZ::Vector3 scaleReciprocal = nonUniformScale.GetReciprocal(); return AZ::Vector3(Round3(scaleReciprocal.GetX()), Round3(scaleReciprocal.GetY()), Round3(scaleReciprocal.GetZ())); } } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp index 83a76aeb01..e5179f4229 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp @@ -373,17 +373,25 @@ namespace AzToolsFramework } } - void Instance::GetConstNestedEntities(const AZStd::function& callback) + bool Instance::GetEntities_Impl(const AZStd::function&)>& callback) { - GetConstEntities(callback); - - for (const auto& [instanceAlias, instance] : m_nestedInstances) + for (auto& [entityAlias, entity] : m_entities) { - instance->GetConstNestedEntities(callback); + if (!entity) + { + continue; + } + + if (!callback(entity)) + { + return false; + } } + + return true; } - void Instance::GetConstEntities(const AZStd::function& callback) + bool Instance::GetConstEntities_Impl(const AZStd::function& callback) const { for (const auto& [entityAlias, entity] : m_entities) { @@ -394,65 +402,91 @@ namespace AzToolsFramework if (!callback(*entity)) { - break; + return false; } } + + return true; } - void Instance::GetNestedEntities(const AZStd::function&)>& callback) + bool Instance::GetAllEntitiesInHierarchy_Impl(const AZStd::function&)>& callback) { - GetEntities(callback); - - for (auto& [instanceAlias, instance] : m_nestedInstances) + if (HasContainerEntity()) { - instance->GetNestedEntities(callback); + if (!callback(m_containerEntity)) + { + return false; + } } - } - void Instance::GetNestedInstances(const AZStd::function&)>& callback) - { - for (auto& [instanceAlias, instance] : m_nestedInstances) + if (!GetEntities_Impl(callback)) { - callback(instance); + return false; } - } - void Instance::GetEntities(const AZStd::function&)>& callback) - { - for (auto& [entityAlias, entity] : m_entities) + for (auto& [instanceAlias, instance] : m_nestedInstances) { - if (!callback(entity)) + if (!instance->GetAllEntitiesInHierarchy_Impl(callback)) { - break; + return false; } } + + return true; } - void Instance::GetEntities(EntityList& entities, bool includeNestedEntities) + bool Instance::GetAllEntitiesInHierarchyConst_Impl(const AZStd::function& callback) const { - // Non-recursive traversal of instances - AZStd::vector instancesToTraverse = { this }; - while (!instancesToTraverse.empty()) + if (HasContainerEntity()) { - Instance* currentInstance = instancesToTraverse.back(); - instancesToTraverse.pop_back(); - if (includeNestedEntities) + if (!callback(*m_containerEntity)) { - instancesToTraverse.reserve(instancesToTraverse.size() + currentInstance->m_nestedInstances.size()); - for (const auto& instanceByAlias : currentInstance->m_nestedInstances) - { - instancesToTraverse.push_back(instanceByAlias.second.get()); - } + return false; } + } - // Size increases by 1 for each instance because we have to count the container entity also. - entities.reserve(entities.size() + currentInstance->m_entities.size() + 1); - entities.push_back(m_containerEntity.get()); - for (const auto& entityByAlias : currentInstance->m_entities) + if (!GetConstEntities_Impl(callback)) + { + return false; + } + + for (const auto& [instanceAlias, instance] : m_nestedInstances) + { + if (!instance->GetAllEntitiesInHierarchyConst_Impl(callback)) { - entities.push_back(entityByAlias.second.get()); + return false; } } + + return true; + } + + void Instance::GetEntities(const AZStd::function&)>& callback) + { + GetEntities_Impl(callback); + } + + void Instance::GetConstEntities(const AZStd::function& callback) const + { + GetConstEntities_Impl(callback); + } + + void Instance::GetAllEntitiesInHierarchy(const AZStd::function&)>& callback) + { + GetAllEntitiesInHierarchy_Impl(callback); + } + + void Instance::GetAllEntitiesInHierarchyConst(const AZStd::function& callback) const + { + GetAllEntitiesInHierarchyConst_Impl(callback); + } + + void Instance::GetNestedInstances(const AZStd::function&)>& callback) + { + for (auto& [instanceAlias, instance] : m_nestedInstances) + { + callback(instance); + } } EntityAliasOptionalReference Instance::GetEntityAlias(const AZ::EntityId& id) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h index 5f36ac10dc..9d3ae31796 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h @@ -121,10 +121,10 @@ namespace AzToolsFramework /** * Gets the entities in the Instance DOM. Can recursively trace all nested instances. */ - void GetConstNestedEntities(const AZStd::function& callback); - void GetConstEntities(const AZStd::function& callback); - void GetNestedEntities(const AZStd::function&)>& callback); void GetEntities(const AZStd::function&)>& callback); + void GetConstEntities(const AZStd::function& callback) const; + void GetAllEntitiesInHierarchy(const AZStd::function&)>& callback); + void GetAllEntitiesInHierarchyConst(const AZStd::function& callback) const; void GetNestedInstances(const AZStd::function&)>& callback); /** @@ -184,12 +184,6 @@ namespace AzToolsFramework static InstanceAlias GenerateInstanceAlias(); - protected: - /** - * Gets the entities owned by this instance - */ - void GetEntities(EntityList& entities, bool includeNestedEntities = false); - private: static constexpr const char s_aliasPathSeparator = '/'; @@ -197,6 +191,11 @@ namespace AzToolsFramework void RemoveEntities(const AZStd::function&)>& filter); + bool GetEntities_Impl(const AZStd::function&)>& callback); + bool GetConstEntities_Impl(const AZStd::function& callback) const; + bool GetAllEntitiesInHierarchy_Impl(const AZStd::function&)>& callback); + bool GetAllEntitiesInHierarchyConst_Impl(const AZStd::function& callback) const; + bool RegisterEntity(const AZ::EntityId& entityId, const EntityAlias& entityAlias); AZStd::unique_ptr DetachEntity(const EntityAlias& entityAlias); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.cpp index 6480ac37d7..cc04fe24c1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.cpp @@ -62,25 +62,16 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils } } - AZStd::vector EditorInfoRemover::GetEntitiesFromInstance(AZStd::unique_ptr& instance) + void EditorInfoRemover::GetEntitiesFromInstance( + AZStd::unique_ptr& instance, EntityList& hierarchyEntities) { - AZStd::vector result; - - instance->GetNestedEntities( - [&result](const AZStd::unique_ptr& entity) + instance->GetAllEntitiesInHierarchy( + [&hierarchyEntities](const AZStd::unique_ptr& entity) { - result.emplace_back(entity.get()); + hierarchyEntities.emplace_back(entity.get()); return true; } ); - - if (instance->HasContainerEntity()) - { - auto containerEntityReference = instance->GetContainerEntity(); - result.emplace_back(&containerEntityReference->get()); - } - - return result; } void EditorInfoRemover::SetEditorOnlyEntityHandlerFromCandidates(const EntityList& entities) @@ -543,7 +534,9 @@ exportComponent, prefabProcessorContext); } // grab all nested entities from the Instance as source entities. - EntityList sourceEntities = GetEntitiesFromInstance(instance); + EntityList sourceEntities; + GetEntitiesFromInstance(instance, sourceEntities); + EntityList exportEntities; // prepare for validation of component requirements. @@ -616,7 +609,7 @@ exportComponent, prefabProcessorContext); ); // replace entities of instance with exported ones. - instance->GetNestedEntities( + instance->GetAllEntitiesInHierarchy( [&exportEntitiesMap](AZStd::unique_ptr& entity) { auto entityId = entity->GetId(); @@ -625,14 +618,6 @@ exportComponent, prefabProcessorContext); } ); - if (instance->HasContainerEntity()) - { - if (auto found = exportEntitiesMap.find(instance->GetContainerEntityId()); found != exportEntitiesMap.end()) - { - instance->SetContainerEntity(*found->second); - } - } - // save the final result in the target Prefab DOM. PrefabDom filteredPrefab; if (!PrefabDomUtils::StoreInstanceInPrefabDom(*instance, filteredPrefab)) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.h index 1e00485e42..5de5c516f8 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/EditorInfoRemover.h @@ -55,8 +55,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils protected: using EntityList = AZStd::vector; - static EntityList GetEntitiesFromInstance( - AZStd::unique_ptr& instance); + static void GetEntitiesFromInstance( + AZStd::unique_ptr& instance, EntityList& hierarchyEntities); static bool ReadComponentAttribute( AZ::Component* component, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/LevelRootUiHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/LevelRootUiHandler.cpp index 7915403c0a..d6b7aebddc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/LevelRootUiHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/LevelRootUiHandler.cpp @@ -58,8 +58,17 @@ namespace AzToolsFramework if (!path.empty()) { - infoString = - QObject::tr("(%1)").arg(path.Filename().Native().data()); + QString saveFlag = ""; + auto dirtyOutcome = m_prefabPublicInterface->HasUnsavedChanges(path); + + if (dirtyOutcome.IsSuccess() && dirtyOutcome.GetValue() == true) + { + saveFlag = "*"; + } + + infoString = QObject::tr("(%1%2)") + .arg(path.Filename().Native().data()) + .arg(saveFlag); } return infoString; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 77918565e4..54ad96535f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -588,15 +589,6 @@ namespace AzToolsFramework bool PrefabIntegrationManager::QueryUserForPrefabFilePath(AZStd::string& outPrefabFilePath) { - QWidget* mainWindow = nullptr; - EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow); - - if (mainWindow == nullptr) - { - AZ_Assert(false, "Prefab - Could not detect Editor main window to generate the asset picker."); - return false; - } - AssetSelectionModel selection; // Note, stringfilter will match every source file CONTAINING ".prefab". @@ -624,7 +616,7 @@ namespace AzToolsFramework selection.SetDisplayFilter(compositeFilterPtr); selection.SetSelectionFilter(compositeFilterPtr); - AssetBrowserComponentRequestBus::Broadcast(&AssetBrowserComponentRequests::PickAssets, selection, mainWindow); + AssetBrowserComponentRequestBus::Broadcast(&AssetBrowserComponentRequests::PickAssets, selection, AzToolsFramework::GetActiveWindow()); if (!selection.IsValid()) { @@ -983,12 +975,7 @@ namespace AzToolsFramework includedEntities.c_str(), referencedEntities.c_str()); - QWidget* mainWindow = nullptr; - AzToolsFramework::EditorRequests::Bus::BroadcastResult( - mainWindow, - &AzToolsFramework::EditorRequests::Bus::Events::GetMainWindow); - - QMessageBox msgBox(mainWindow); + QMessageBox msgBox(AzToolsFramework::GetActiveWindow()); msgBox.setWindowTitle("External Entity References"); msgBox.setText("The prefab contains references to external entities that are not selected."); msgBox.setInformativeText("You can move the referenced entities into this prefab or retain the external references."); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h index 85250f2a32..d202735522 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h @@ -210,6 +210,15 @@ namespace AzToolsFramework //! Type to inherit to implement ViewportInteractionRequests. using ViewportInteractionRequestBus = AZ::EBus; + //! An interface to notify when changes to viewport settings have happened. + class ViewportSettingNotifications + { + public: + virtual void OnGridSnappingChanged(bool enabled) = 0; + }; + + using ViewportSettingsNotificationBus = AZ::EBus; + //! Requests to freeze the Viewport Input //! Added to prevent a bug with the legacy CryEngine Viewport code that would //! keep doing raycast tests even when no level is loaded, causing a crash. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp index eb16bf158e..54934c13c7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp @@ -127,6 +127,7 @@ namespace AzToolsFramework static const char* const s_dittoTranslationIndividualUndoRedoDesc = "Ditto translation individual"; static const char* const s_dittoScaleIndividualWorldUndoRedoDesc = "Ditto scale individual world"; static const char* const s_dittoScaleIndividualLocalUndoRedoDesc = "Ditto scale individual local"; + static const char* const s_snapToWorldGridUndoRedoDesc = "Snap to world grid"; static const char* const s_showAllEntitiesUndoRedoDesc = s_showAllTitle; static const char* const s_lockSelectionUndoRedoDesc = s_lockSelectionTitle; static const char* const s_hideSelectionUndoRedoDesc = s_hideSelectionTitle; @@ -142,6 +143,7 @@ namespace AzToolsFramework static const char* const SpaceClusterWorldTooltip = "Toggle world space lock"; static const char* const SpaceClusterParentTooltip = "Toggle parent space lock"; static const char* const SpaceClusterLocalTooltip = "Toggle local space lock"; + static const char* const SnappingClusterSnapToWorldTooltip = "Snap selected entities to the world space grid"; static const AZ::Color s_fadedXAxisColor = AZ::Color(AZ::u8(200), AZ::u8(127), AZ::u8(127), AZ::u8(255)); static const AZ::Color s_fadedYAxisColor = AZ::Color(AZ::u8(127), AZ::u8(190), AZ::u8(127), AZ::u8(255)); @@ -150,8 +152,6 @@ namespace AzToolsFramework static const AZ::Color s_pickedOrientationColor = AZ::Color(0.0f, 1.0f, 0.0f, 1.0f); static const AZ::Color s_selectedEntityAabbColor = AZ::Color(0.6f, 0.6f, 0.6f, 0.4f); - static const int s_defaultViewportId = 0; - static const float s_pivotSize = 0.075f; // the size of the pivot (box) to render when selected // data passed to manipulators when processing mouse interactions @@ -489,6 +489,16 @@ namespace AzToolsFramework return buttonId; } + void SnappingCluster::TrySetVisible(const bool visible) + { + bool snapping = false; + ViewportInteraction::ViewportInteractionRequestBus::EventResult( + snapping, ViewportUi::DefaultViewportId, &ViewportInteraction::ViewportInteractionRequestBus::Events::GridSnappingEnabled); + + // show snapping viewport ui only if there are entities selected and snapping is enabled + SetViewportUiClusterVisible(m_clusterId, visible && snapping); + } + // return either center or entity pivot static AZ::Vector3 CalculatePivotTranslation(const AZ::EntityId entityId, const EditorTransformComponentSelectionRequests::Pivot pivot) { @@ -503,7 +513,8 @@ namespace AzToolsFramework void EditorTransformComponentSelection::SetAllViewportUiVisible(const bool visible) { SetViewportUiClusterVisible(m_transformModeClusterId, visible); - SetViewportUiClusterVisible(m_spaceCluster.m_spaceClusterId, visible); + SetViewportUiClusterVisible(m_spaceCluster.m_clusterId, visible); + SetViewportUiClusterVisible(m_snappingCluster.m_clusterId, visible); m_viewportUiVisible = visible; } @@ -524,8 +535,8 @@ namespace AzToolsFramework }; ViewportUi::ViewportUiRequestBus::Event( - ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterActiveButton, - m_spaceCluster.m_spaceClusterId, buttonIdFromFrameFn(referenceFrame)); + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterActiveButton, m_spaceCluster.m_clusterId, + buttonIdFromFrameFn(referenceFrame)); } namespace ETCS @@ -1034,9 +1045,12 @@ namespace AzToolsFramework EditorEntityLockComponentNotificationBus::Router::BusRouterConnect(); EditorManipulatorCommandUndoRedoRequestBus::Handler::BusConnect(entityContextId); EditorContextMenuBus::Handler::BusConnect(); + ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusConnect(ViewportUi::DefaultViewportId); CreateTransformModeSelectionCluster(); CreateSpaceSelectionCluster(); + CreateSnappingCluster(); + RegisterActions(); SetupBoxSelect(); RefreshSelectedEntityIdsAndRegenerateManipulators(); @@ -1048,12 +1062,14 @@ namespace AzToolsFramework DestroyManipulators(m_entityIdManipulators); DestroyCluster(m_transformModeClusterId); - DestroyCluster(m_spaceCluster.m_spaceClusterId); + DestroyCluster(m_spaceCluster.m_clusterId); + DestroyCluster(m_snappingCluster.m_clusterId); UnregisterActions(); m_pivotOverrideFrame.Reset(); + ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusDisconnect(); EditorContextMenuBus::Handler::BusConnect(); EditorManipulatorCommandUndoRedoRequestBus::Handler::BusDisconnect(); EditorEntityLockComponentNotificationBus::Router::BusRouterDisconnect(); @@ -2473,7 +2489,7 @@ namespace AzToolsFramework // create and register the buttons (strings correspond to icons even if the values appear different) m_translateButtonId = RegisterClusterButton(m_transformModeClusterId, "Move"); - m_rotateButtonId = RegisterClusterButton(m_transformModeClusterId, "Translate"); + m_rotateButtonId = RegisterClusterButton(m_transformModeClusterId, "Rotate"); m_scaleButtonId = RegisterClusterButton(m_transformModeClusterId, "Scale"); // set button tooltips @@ -2513,28 +2529,64 @@ namespace AzToolsFramework m_transformModeSelectionHandler); } + void EditorTransformComponentSelection::CreateSnappingCluster() + { + // create the cluster for switching spaces/reference frames + ViewportUi::ViewportUiRequestBus::EventResult( + m_snappingCluster.m_clusterId, ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateCluster, + ViewportUi::Alignment::TopRight); + + m_snappingCluster.m_snapToWorldButtonId = RegisterClusterButton(m_snappingCluster.m_clusterId, "Grid"); + + // set button tooltips + ViewportUi::ViewportUiRequestBus::Event( + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, + m_snappingCluster.m_clusterId, m_snappingCluster.m_snapToWorldButtonId, SnappingClusterSnapToWorldTooltip); + + const auto onButtonClicked = [this](const ViewportUi::ButtonId buttonId) + { + if (buttonId == m_snappingCluster.m_snapToWorldButtonId) + { + float gridSize = 1.0f; + ViewportInteraction::ViewportInteractionRequestBus::EventResult( + gridSize, ViewportUi::DefaultViewportId, &ViewportInteraction::ViewportInteractionRequestBus::Events::GridSize); + + SnapSelectedEntitiesToWorldGrid(gridSize); + } + }; + + m_snappingCluster.m_snappingHandler = AZ::Event::Handler(onButtonClicked); + + ViewportUi::ViewportUiRequestBus::Event( + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RegisterClusterEventHandler, + m_snappingCluster.m_clusterId, m_snappingCluster.m_snappingHandler); + + // hide initially + SetViewportUiClusterVisible(m_snappingCluster.m_clusterId, false); + } + void EditorTransformComponentSelection::CreateSpaceSelectionCluster() { // create the cluster for switching spaces/reference frames ViewportUi::ViewportUiRequestBus::EventResult( - m_spaceCluster.m_spaceClusterId, ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateCluster, + m_spaceCluster.m_clusterId, ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateCluster, ViewportUi::Alignment::TopRight); // create and register the buttons (strings correspond to icons even if the values appear different) - m_spaceCluster.m_worldButtonId = RegisterClusterButton(m_spaceCluster.m_spaceClusterId, "World"); - m_spaceCluster.m_parentButtonId = RegisterClusterButton(m_spaceCluster.m_spaceClusterId, "Parent"); - m_spaceCluster.m_localButtonId = RegisterClusterButton(m_spaceCluster.m_spaceClusterId, "Local"); + m_spaceCluster.m_worldButtonId = RegisterClusterButton(m_spaceCluster.m_clusterId, "World"); + m_spaceCluster.m_parentButtonId = RegisterClusterButton(m_spaceCluster.m_clusterId, "Parent"); + m_spaceCluster.m_localButtonId = RegisterClusterButton(m_spaceCluster.m_clusterId, "Local"); // set button tooltips ViewportUi::ViewportUiRequestBus::Event( - ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, - m_spaceCluster.m_spaceClusterId, m_spaceCluster.m_worldButtonId, SpaceClusterWorldTooltip); + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, m_spaceCluster.m_clusterId, + m_spaceCluster.m_worldButtonId, SpaceClusterWorldTooltip); ViewportUi::ViewportUiRequestBus::Event( - ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, - m_spaceCluster.m_spaceClusterId, m_spaceCluster.m_parentButtonId, SpaceClusterParentTooltip); + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, m_spaceCluster.m_clusterId, + m_spaceCluster.m_parentButtonId, SpaceClusterParentTooltip); ViewportUi::ViewportUiRequestBus::Event( - ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, - m_spaceCluster.m_spaceClusterId, m_spaceCluster.m_localButtonId, SpaceClusterLocalTooltip); + ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonTooltip, m_spaceCluster.m_clusterId, + m_spaceCluster.m_localButtonId, SpaceClusterLocalTooltip); auto onButtonClicked = [this](ViewportUi::ButtonId buttonId) { @@ -2576,14 +2628,31 @@ namespace AzToolsFramework } ViewportUi::ViewportUiRequestBus::Event( ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::SetClusterButtonLocked, - m_spaceCluster.m_spaceClusterId, buttonId, m_spaceCluster.m_spaceLock.has_value()); + m_spaceCluster.m_clusterId, buttonId, m_spaceCluster.m_spaceLock.has_value()); }; - m_spaceCluster.m_spaceSelectionHandler = AZ::Event::Handler(onButtonClicked); + m_spaceCluster.m_spaceHandler = AZ::Event::Handler(onButtonClicked); ViewportUi::ViewportUiRequestBus::Event( ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RegisterClusterEventHandler, - m_spaceCluster.m_spaceClusterId, m_spaceCluster.m_spaceSelectionHandler); + m_spaceCluster.m_clusterId, m_spaceCluster.m_spaceHandler); + } + + void EditorTransformComponentSelection::SnapSelectedEntitiesToWorldGrid(const float gridSize) + { + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); + + const AZStd::array snapAxes = { AZ::Vector3::CreateAxisX(), AZ::Vector3::CreateAxisY(), AZ::Vector3::CreateAxisZ() }; + + ScopedUndoBatch undoBatch(s_snapToWorldGridUndoRedoDesc); + for (const AZ::EntityId& entityId : m_selectedEntityIds) + { + ScopedUndoBatch::MarkEntityDirty(entityId); + SetEntityWorldTranslation( + entityId, CalculateSnappedPosition(GetWorldTranslation(entityId), snapAxes.data(), snapAxes.size(), gridSize)); + } + + RefreshManipulators(RefreshType::Translation); } EditorTransformComponentSelectionRequests::Mode EditorTransformComponentSelection::GetTransformMode() @@ -3145,15 +3214,16 @@ namespace AzToolsFramework return "Transform Component"; } - void EditorTransformComponentSelection::PopulateEditorGlobalContextMenu(QMenu* menu, [[maybe_unused]] const AZ::Vector2& point, [[maybe_unused]] int flags) + void EditorTransformComponentSelection::PopulateEditorGlobalContextMenu( + QMenu* menu, [[maybe_unused]] const AZ::Vector2& point, [[maybe_unused]] int flags) { - QAction* action = menu->addAction(QObject::tr(s_togglePivotTitleRightClick)); - QObject::connect( - action, &QAction::triggered, action, - [this]() - { - ToggleCenterPivotSelection(); - }); + QAction* action = menu->addAction(QObject::tr(s_togglePivotTitleRightClick)); + QObject::connect( + action, &QAction::triggered, action, + [this]() + { + ToggleCenterPivotSelection(); + }); } void EditorTransformComponentSelection::BeforeEntitySelectionChanged() @@ -3175,7 +3245,7 @@ namespace AzToolsFramework } void EditorTransformComponentSelection::AfterEntitySelectionChanged( - const EntityIdList& /*newlySelectedEntities*/, const EntityIdList& /*newlyDeselectedEntities*/) + [[maybe_unused]] const EntityIdList& newlySelectedEntities, [[maybe_unused]] const EntityIdList& newlyDeselectedEntities) { AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); @@ -3195,6 +3265,8 @@ namespace AzToolsFramework m_didSetSelectedEntities = false; } + m_snappingCluster.TrySetVisible(m_viewportUiVisible && !m_selectedEntityIds.empty()); + RegenerateManipulators(); } @@ -3657,6 +3729,11 @@ namespace AzToolsFramework SetAllViewportUiVisible(true); } + void EditorTransformComponentSelection::OnGridSnappingChanged([[maybe_unused]] const bool enabled) + { + m_snappingCluster.TrySetVisible(m_viewportUiVisible && !m_selectedEntityIds.empty()); + } + namespace ETCS { // little raii wrapper to switch a value from true to false and back diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h index 773921ebea..00c2cb4e50 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.h @@ -115,12 +115,28 @@ namespace AzToolsFramework SpaceCluster(const SpaceCluster&) = delete; SpaceCluster& operator=(const SpaceCluster&) = delete; - ViewportUi::ClusterId m_spaceClusterId; //!< The id identifying the reference space cluster. + ViewportUi::ClusterId m_clusterId; //!< The id identifying the reference space cluster. ViewportUi::ButtonId m_localButtonId; //!< Local reference space button id. ViewportUi::ButtonId m_parentButtonId; //!< Parent reference space button id. ViewportUi::ButtonId m_worldButtonId; //!< World reference space button id. - AZ::Event::Handler m_spaceSelectionHandler; //!< Callback for when a space cluster button is pressed. AZStd::optional m_spaceLock; //!< Locked reference frame to use if set. + AZ::Event::Handler m_spaceHandler; //!< Callback for when a space cluster button is pressed. + }; + + //! Grouping of viewport ui related state for aligning transforms to a grid. + struct SnappingCluster + { + SnappingCluster() = default; + // disable copying and moving (implicit) + SnappingCluster(const SnappingCluster&) = delete; + SnappingCluster& operator=(const SnappingCluster&) = delete; + + //! Attempt to show the snapping cluster (will only succeed if snapping is enabled). + void TrySetVisible(bool visible); + + ViewportUi::ClusterId m_clusterId; //!< The cluster id for all snapping buttons. + ViewportUi::ButtonId m_snapToWorldButtonId; //!< The button id for snapping all axes to the world. + AZ::Event::Handler m_snappingHandler; //!< Callback for when a snapping cluster button is pressed. }; //! Entity selection/interaction handling. @@ -138,6 +154,7 @@ namespace AzToolsFramework , private EditorEntityLockComponentNotificationBus::Router , private EditorManipulatorCommandUndoRedoRequestBus::Handler , private AZ::TransformNotificationBus::MultiHandler + , private ViewportInteraction::ViewportSettingsNotificationBus::Handler { public: AZ_CLASS_ALLOCATOR_DECL @@ -180,6 +197,7 @@ namespace AzToolsFramework void CreateTransformModeSelectionCluster(); void CreateSpaceSelectionCluster(); + void CreateSnappingCluster(); void ClearManipulatorTranslationOverride(); void ClearManipulatorOrientationOverride(); @@ -228,14 +246,15 @@ namespace AzToolsFramework AZStd::optional GetManipulatorTransform() override; void OverrideManipulatorOrientation(const AZ::Quaternion& orientation) override; void OverrideManipulatorTranslation(const AZ::Vector3& translation) override; - void CopyTranslationToSelectedEntitiesIndividual(const AZ::Vector3& translation); - void CopyTranslationToSelectedEntitiesGroup(const AZ::Vector3& translation); - void ResetTranslationForSelectedEntitiesLocal(); - void CopyOrientationToSelectedEntitiesIndividual(const AZ::Quaternion& orientation); - void CopyOrientationToSelectedEntitiesGroup(const AZ::Quaternion& orientation); - void ResetOrientationForSelectedEntitiesLocal(); - void CopyScaleToSelectedEntitiesIndividualLocal(float scale); - void CopyScaleToSelectedEntitiesIndividualWorld(float scale); + void CopyTranslationToSelectedEntitiesIndividual(const AZ::Vector3& translation) override; + void CopyTranslationToSelectedEntitiesGroup(const AZ::Vector3& translation) override; + void ResetTranslationForSelectedEntitiesLocal() override; + void CopyOrientationToSelectedEntitiesIndividual(const AZ::Quaternion& orientation) override; + void CopyOrientationToSelectedEntitiesGroup(const AZ::Quaternion& orientation) override; + void ResetOrientationForSelectedEntitiesLocal() override; + void CopyScaleToSelectedEntitiesIndividualLocal(float scale) override; + void CopyScaleToSelectedEntitiesIndividualWorld(float scale) override; + void SnapSelectedEntitiesToWorldGrid(float gridSize) override; // EditorManipulatorCommandUndoRedoRequestBus ... void UndoRedoEntityManipulatorCommand( @@ -274,6 +293,9 @@ namespace AzToolsFramework void OnStartPlayInEditor() override; void OnStopPlayInEditor() override; + // ViewportSettingsNotificationBus overrides ... + void OnGridSnappingChanged(bool enabled) override; + // Helpers to safely interact with the TransformBus (requests). void SetEntityWorldTranslation(AZ::EntityId entityId, const AZ::Vector3& worldTranslation); void SetEntityLocalTranslation(AZ::EntityId entityId, const AZ::Vector3& localTranslation); @@ -320,6 +342,7 @@ namespace AzToolsFramework AzFramework::ClickDetector m_clickDetector; //!< Detect different types of mouse click. AzFramework::CursorState m_cursorState; //!< Track the mouse position and delta movement each frame. SpaceCluster m_spaceCluster; //!< Related viewport ui state for controlling the current reference space. + SnappingCluster m_snappingCluster; //!< Related viewport ui state for aligning positions to a grid or reference frame. bool m_viewportUiVisible = true; //!< Used to hide/show the viewport ui elements. }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h index 966f9333fc..c7b75da206 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h @@ -104,6 +104,9 @@ namespace AzToolsFramework //! Copy scale to to each individual entity in world (absolute) space. virtual void CopyScaleToSelectedEntitiesIndividualWorld(float scale) = 0; + //! Snap selected entities to be aligned with the world space grid. + virtual void SnapSelectedEntitiesToWorldGrid(float gridSize) = 0; + protected: ~EditorTransformComponentSelectionRequests() = default; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp index 7b2d45e652..81be2bba90 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp @@ -24,8 +24,6 @@ namespace AzToolsFramework::ViewportUi::Internal { - // margin for the Viewport UI Overlay in pixels - const static int ViewportUiOverlayMargin = 5; const static int HighlightBorderSize = 5; const static int TopHighlightBorderSize = 25; const static char* HighlightBorderColor = "#44B2F8"; @@ -281,7 +279,7 @@ namespace AzToolsFramework::ViewportUi::Internal void ViewportUiDisplay::HideViewportUiElement(ViewportUiElementId elementId) { if (ViewportUiElementInfo element = GetViewportUiElementInfo(elementId); - element.m_widget && UiDisplayEnabled()) + element.m_widget) { element.m_widget->setVisible(false); } @@ -387,7 +385,6 @@ namespace AzToolsFramework::ViewportUi::Internal m_fullScreenLayout.setSpacing(0); m_fullScreenLayout.setContentsMargins(0, 0, 0, 0); m_fullScreenLayout.addLayout(&m_uiOverlayLayout, 0, 0, 1, 1); - m_uiOverlayLayout.setMargin(ViewportUiOverlayMargin); // format the label which will appear on top of the highlight border AZStd::string styleSheet = AZStd::string::format( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.cpp index bab664d832..92fad8cc93 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.cpp @@ -25,13 +25,15 @@ namespace AzToolsFramework::ViewportUi::Internal : QGridLayout(parent) { // set margins and spacing for internal contents - setContentsMargins(0, 0, 0, 0); + setContentsMargins( + ViewportUiOverlayMargin, ViewportUiOverlayMargin + ViewportUiOverlayTopMarginPadding, ViewportUiOverlayMargin, + ViewportUiOverlayMargin); setSpacing(ViewportUiDisplayLayoutSpacing); // create a 3x2 map of sub layouts which will stack widgets according to their mapped alignment m_internalLayouts = AZStd::unordered_map { CreateSubLayout(new QVBoxLayout(), 0, 0, Qt::AlignTop | Qt::AlignLeft), - CreateSubLayout(new QHBoxLayout(), 1, 0, Qt::AlignBottom | Qt::AlignLeft), + CreateSubLayout(new QVBoxLayout(), 1, 0, Qt::AlignBottom | Qt::AlignLeft), CreateSubLayout(new QVBoxLayout(), 0, 1, Qt::AlignTop), CreateSubLayout(new QHBoxLayout(), 1, 1, Qt::AlignBottom), CreateSubLayout(new QVBoxLayout(), 0, 2, Qt::AlignTop | Qt::AlignRight), @@ -50,9 +52,42 @@ namespace AzToolsFramework::ViewportUi::Internal if (auto layoutForAlignment = m_internalLayouts.find(alignment); layoutForAlignment != m_internalLayouts.end()) { - // place the widget before the invisible spacer - // spacer must be last item in layout to not interfere with positioning - int index = layoutForAlignment->second->count() - 1; + // place the widget before or after the invisible spacer + // depending on the layout alignment + int index = 0; + switch (alignment) + { + case Qt::AlignTop | Qt::AlignLeft: + case Qt::AlignTop: + index = layoutForAlignment->second->count() - 1; + break; + case Qt::AlignBottom | Qt::AlignRight: + case Qt::AlignBottom: + index = layoutForAlignment->second->count(); + break; + // TopRight and BottomLeft are special cases + // place the spacer differently according to whether it's a vertical or horizontal layout + case Qt::AlignTop | Qt::AlignRight: + if (QVBoxLayout* vLayout = qobject_cast(layoutForAlignment->second)) + { + index = layoutForAlignment->second->count() - 1; + } + else if (QHBoxLayout* hLayout = qobject_cast(layoutForAlignment->second)) + { + index = layoutForAlignment->second->count(); + } + break; + case Qt::AlignBottom | Qt::AlignLeft: + if (QVBoxLayout* vLayout = qobject_cast(layoutForAlignment->second)) + { + index = layoutForAlignment->second->count(); + } + else if (QHBoxLayout* hLayout = qobject_cast(layoutForAlignment->second)) + { + index = layoutForAlignment->second->count() - 1; + } + break; + } layoutForAlignment->second->insertWidget(index, widget); } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.h index 0beb0d5bd6..8d710264be 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplayLayout.h @@ -19,6 +19,11 @@ namespace AzToolsFramework::ViewportUi::Internal { + // margin for the Viewport UI Overlay in pixels + constexpr int ViewportUiOverlayMargin = 5; + // padding to make space for ImGui + constexpr int ViewportUiOverlayTopMarginPadding = 20; + //! QGridLayout implementation that uses a grid of QVBox/QHBoxLayouts internally to stack widgets. class ViewportUiDisplayLayout : public QGridLayout { diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index e5ac1f9693..9ec0deaed6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -544,6 +544,8 @@ set(FILES AssetBrowser/AssetBrowserEntry.h AssetBrowser/AssetBrowserFilterModel.cpp AssetBrowser/AssetBrowserFilterModel.h + AssetBrowser/AssetBrowserTableModel.cpp + AssetBrowser/AssetBrowserTableModel.h AssetBrowser/AssetBrowserModel.cpp AssetBrowser/AssetBrowserModel.h AssetBrowser/AssetEntryChange.h @@ -554,6 +556,8 @@ set(FILES AssetBrowser/EBusFindAssetTypeByName.h AssetBrowser/Views/AssetBrowserTreeView.cpp AssetBrowser/Views/AssetBrowserTreeView.h + AssetBrowser/Views/AssetBrowserTableView.cpp + AssetBrowser/Views/AssetBrowserTableView.h AssetBrowser/Views/EntryDelegate.cpp AssetBrowser/Views/EntryDelegate.h AssetBrowser/Views/AssetBrowserFolderWidget.cpp diff --git a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp index cbc81ba9b8..9a34efa6f5 100644 --- a/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp @@ -1,42 +1,42 @@ /* -* 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. -* -*/ + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ #include #include #include -#include #include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include -#include #include #include +#include #include #include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include using namespace AzToolsFramework; @@ -46,12 +46,11 @@ namespace AZ { return os << entityId.ToString().c_str(); } -} +} // namespace AZ namespace UnitTest { - class EditorEntityVisibilityCacheFixture - : public ToolsApplicationFixture + class EditorEntityVisibilityCacheFixture : public ToolsApplicationFixture { public: void CreateLayerAndEntityHierarchy() @@ -116,8 +115,7 @@ namespace UnitTest } // Fixture to support testing EditorTransformComponentSelection functionality on an Entity selection. - class EditorTransformComponentSelectionFixture - : public ToolsApplicationFixture + class EditorTransformComponentSelectionFixture : public ToolsApplicationFixture { public: void SetUpEditorFixtureImpl() override @@ -138,13 +136,11 @@ namespace UnitTest EntityIdList m_entityIds; }; - void EditorTransformComponentSelectionFixture::ArrangeIndividualRotatedEntitySelection( - const AZ::Quaternion& orientation) + void EditorTransformComponentSelectionFixture::ArrangeIndividualRotatedEntitySelection(const AZ::Quaternion& orientation) { for (auto entityId : m_entityIds) { - AZ::TransformBus::Event( - entityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, orientation); + AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetLocalRotationQuaternion, orientation); } } @@ -152,40 +148,32 @@ namespace UnitTest { AZStd::optional manipulatorTransform; EditorTransformComponentSelectionRequestBus::EventResult( - manipulatorTransform, GetEntityContextId(), - &EditorTransformComponentSelectionRequests::GetManipulatorTransform); + manipulatorTransform, GetEntityContextId(), &EditorTransformComponentSelectionRequests::GetManipulatorTransform); return manipulatorTransform; } - void EditorTransformComponentSelectionFixture::RefreshManipulators( - EditorTransformComponentSelectionRequests::RefreshType refreshType) + void EditorTransformComponentSelectionFixture::RefreshManipulators(EditorTransformComponentSelectionRequests::RefreshType refreshType) { EditorTransformComponentSelectionRequestBus::Event( GetEntityContextId(), &EditorTransformComponentSelectionRequests::RefreshManipulators, refreshType); } - void EditorTransformComponentSelectionFixture::SetTransformMode( - EditorTransformComponentSelectionRequests::Mode transformMode) + void EditorTransformComponentSelectionFixture::SetTransformMode(EditorTransformComponentSelectionRequests::Mode transformMode) { EditorTransformComponentSelectionRequestBus::Event( - GetEntityContextId(), &EditorTransformComponentSelectionRequests::SetTransformMode, - transformMode); + GetEntityContextId(), &EditorTransformComponentSelectionRequests::SetTransformMode, transformMode); } - void EditorTransformComponentSelectionFixture::OverrideManipulatorOrientation( - const AZ::Quaternion& orientation) + void EditorTransformComponentSelectionFixture::OverrideManipulatorOrientation(const AZ::Quaternion& orientation) { EditorTransformComponentSelectionRequestBus::Event( - GetEntityContextId(), &EditorTransformComponentSelectionRequests::OverrideManipulatorOrientation, - orientation); + GetEntityContextId(), &EditorTransformComponentSelectionRequests::OverrideManipulatorOrientation, orientation); } - void EditorTransformComponentSelectionFixture::OverrideManipulatorTranslation( - const AZ::Vector3& translation) + void EditorTransformComponentSelectionFixture::OverrideManipulatorTranslation(const AZ::Vector3& translation) { EditorTransformComponentSelectionRequestBus::Event( - GetEntityContextId(), &EditorTransformComponentSelectionRequests::OverrideManipulatorTranslation, - translation); + GetEntityContextId(), &EditorTransformComponentSelectionRequests::OverrideManipulatorTranslation, translation); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -202,8 +190,7 @@ namespace UnitTest SetTransformMode(EditorTransformComponentSelectionRequests::Mode::Rotation); - const AZ::Transform manipulatorTransformBefore = - GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); + const AZ::Transform manipulatorTransformBefore = GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); // check preconditions - manipulator transform matches parent/world transform (identity) EXPECT_THAT(manipulatorTransformBefore.GetBasisY(), IsClose(AZ::Vector3::CreateAxisY())); @@ -218,8 +205,7 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Then - const AZ::Transform manipulatorTransformAfter = - GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); + const AZ::Transform manipulatorTransformAfter = GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); // check postconditions - manipulator transform matches parent/world transform (identity) EXPECT_THAT(manipulatorTransformAfter.GetBasisY(), IsClose(AZ::Vector3::CreateAxisY())); @@ -229,8 +215,7 @@ namespace UnitTest { // create invalid starting orientation to guarantee correct data is coming from GetLocalRotationQuaternion AZ::Quaternion entityOrientation = AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisX(), 90.0f); - AZ::TransformBus::EventResult( - entityOrientation, entityId, &AZ::TransformBus::Events::GetLocalRotationQuaternion); + AZ::TransformBus::EventResult(entityOrientation, entityId, &AZ::TransformBus::Events::GetLocalRotationQuaternion); // manipulator orientation matches entity orientation EXPECT_THAT(entityOrientation, IsClose(manipulatorTransformAfter.GetRotation())); @@ -252,8 +237,7 @@ namespace UnitTest SetTransformMode(EditorTransformComponentSelectionRequests::Mode::Rotation); - const AZ::Transform manipulatorTransformBefore = - GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); + const AZ::Transform manipulatorTransformBefore = GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); // check preconditions - manipulator transform matches manipulator orientation override (not entity transform) EXPECT_THAT(manipulatorTransformBefore.GetBasisX(), IsClose(AZ::Vector3::CreateAxisY())); @@ -268,8 +252,7 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Then - const AZ::Transform manipulatorTransformAfter = - GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); + const AZ::Transform manipulatorTransformAfter = GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()); // check postconditions - manipulator transform matches parent/world space (manipulator override was cleared) EXPECT_THAT(manipulatorTransformAfter.GetBasisY(), IsClose(AZ::Vector3::CreateAxisY())); @@ -278,8 +261,7 @@ namespace UnitTest for (auto entityId : m_entityIds) { AZ::Quaternion entityOrientation; - AZ::TransformBus::EventResult( - entityOrientation, entityId, &AZ::TransformBus::Events::GetLocalRotationQuaternion); + AZ::TransformBus::EventResult(entityOrientation, entityId, &AZ::TransformBus::Events::GetLocalRotationQuaternion); // entity transform matches initial (entity transform was not reset, only manipulator was) EXPECT_THAT(entityOrientation, IsClose(initialEntityOrientation)); @@ -301,16 +283,13 @@ namespace UnitTest AZ::EntityId parentId = CreateDefaultEditorEntity("Parent", &parent); AZ::EntityId childId = CreateDefaultEditorEntity("Child", &child); - AZ::TransformBus::Event( - childId, &AZ::TransformInterface::SetParent, parentId); - AZ::TransformBus::Event( - parentId, &AZ::TransformInterface::SetParent, grandParentId); + AZ::TransformBus::Event(childId, &AZ::TransformInterface::SetParent, parentId); + AZ::TransformBus::Event(parentId, &AZ::TransformInterface::SetParent, grandParentId); UnitTest::SliceAssets sliceAssets; const auto sliceAssetId = UnitTest::SaveAsSlice({ grandParent }, GetApplication(), sliceAssets); - EntityList instantiatedEntities = - UnitTest::InstantiateSlice(sliceAssetId, sliceAssets); + EntityList instantiatedEntities = UnitTest::InstantiateSlice(sliceAssetId, sliceAssets); const AZ::EntityId entityIdToMove = instantiatedEntities.back()->GetId(); EditorEntityComponentChangeDetector editorEntityChangeDetector(entityIdToMove); @@ -321,8 +300,7 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // When EditorTransformComponentSelectionRequestBus::Event( - GetEntityContextId(), - &EditorTransformComponentSelectionRequests::CopyOrientationToSelectedEntitiesIndividual, + GetEntityContextId(), &EditorTransformComponentSelectionRequests::CopyOrientationToSelectedEntitiesIndividual, AZ::Quaternion::CreateFromAxisAngle(AZ::Vector3::CreateAxisX(), AZ::DegToRad(90.0f))); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -362,10 +340,9 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Then AzToolsFramework::EntityIdList selectedEntities; - ToolsApplicationRequestBus::BroadcastResult( - selectedEntities, &ToolsApplicationRequestBus::Events::GetSelectedEntities); + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequestBus::Events::GetSelectedEntities); - AzToolsFramework::EntityIdList expectedSelectedEntities = {entity4, entity5, entity6}; + AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 }; EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities)); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -396,10 +373,9 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Then AzToolsFramework::EntityIdList selectedEntities; - ToolsApplicationRequestBus::BroadcastResult( - selectedEntities, &ToolsApplicationRequestBus::Events::GetSelectedEntities); + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequestBus::Events::GetSelectedEntities); - AzToolsFramework::EntityIdList expectedSelectedEntities = {m_entity1, entity2, entity3, entity4}; + AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entity1, entity2, entity3, entity4 }; EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities)); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -416,11 +392,9 @@ namespace UnitTest const auto finalTransformWorld = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 0.0f)); // calculate the position in screen space of the initial position of the entity - const auto initialPositionScreen = - AzFramework::WorldToScreen(initialTransformWorld.GetTranslation(), m_cameraState); + const auto initialPositionScreen = AzFramework::WorldToScreen(initialTransformWorld.GetTranslation(), m_cameraState); // calculate the position in screen space of the final position of the entity - const auto finalPositionScreen = - AzFramework::WorldToScreen(finalTransformWorld.GetTranslation(), m_cameraState); + const auto finalPositionScreen = AzFramework::WorldToScreen(finalTransformWorld.GetTranslation(), m_cameraState); // select the entity (this will cause the manipulators to appear in EditorTransformComponentSelection) AzToolsFramework::SelectEntity(m_entity1); @@ -452,10 +426,10 @@ namespace UnitTest } // simple widget to listen for a mouse wheel event and then forward it on to the ViewportSelectionRequestBus - class WheelEventWidget - : public QWidget + class WheelEventWidget : public QWidget { using MouseInteractionResult = AzToolsFramework::ViewportInteraction::MouseInteractionResult; + public: WheelEventWidget(QWidget* parent = nullptr) : QWidget(parent) @@ -490,8 +464,7 @@ namespace UnitTest { EditorTransformComponentSelectionRequests::Mode transformMode; EditorTransformComponentSelectionRequestBus::EventResult( - transformMode, GetEntityContextId(), - &EditorTransformComponentSelectionRequestBus::Events::GetTransformMode); + transformMode, GetEntityContextId(), &EditorTransformComponentSelectionRequestBus::Events::GetTransformMode); return transformMode; }; @@ -519,6 +492,56 @@ namespace UnitTest EXPECT_THAT(wheelEventWidget.m_mouseInteractionResult, Eq(vi::MouseInteractionResult::Viewport)); } + TEST_F(EditorTransformComponentSelectionFixture, EntityPositionsCanBeSnappedToGrid) + { + using ::testing::Pointwise; + + m_entityIds.push_back(CreateDefaultEditorEntity("Entity2")); + m_entityIds.push_back(CreateDefaultEditorEntity("Entity3")); + + const AZStd::vector initialUnsnappedPositions = { AZ::Vector3(1.2f, 3.5f, 6.7f), AZ::Vector3(13.2f, 15.6f, 11.4f), + AZ::Vector3(4.2f, 103.2f, 16.6f) }; + AZ::TransformBus::Event(m_entityIds[0], &AZ::TransformBus::Events::SetWorldTranslation, initialUnsnappedPositions[0]); + AZ::TransformBus::Event(m_entityIds[1], &AZ::TransformBus::Events::SetWorldTranslation, initialUnsnappedPositions[1]); + AZ::TransformBus::Event(m_entityIds[2], &AZ::TransformBus::Events::SetWorldTranslation, initialUnsnappedPositions[2]); + + AzToolsFramework::SelectEntities(m_entityIds); + + EditorTransformComponentSelectionRequestBus::Event( + GetEntityContextId(), &EditorTransformComponentSelectionRequestBus::Events::SnapSelectedEntitiesToWorldGrid, 2.0f); + + AZStd::vector entityPositionsAfterSnap; + AZStd::transform( + m_entityIds.cbegin(), m_entityIds.cend(), AZStd::back_inserter(entityPositionsAfterSnap), + [](const AZ::EntityId& entityId) + { + return GetWorldTranslation(entityId); + }); + + const AZStd::vector expectedSnappedPositions = { AZ::Vector3(2.0f, 4.0f, 6.0f), AZ::Vector3(14.0f, 16.0f, 12.0f), + AZ::Vector3(4.0f, 104.0f, 16.0f) }; + EXPECT_THAT(entityPositionsAfterSnap, Pointwise(ContainerIsClose(), expectedSnappedPositions)); + } + + TEST_F(EditorTransformComponentSelectionFixture, ManipulatorStaysAlignedToEntityTranslationAfterSnap) + { + const auto initialUnsnappedPosition = AZ::Vector3(1.2f, 3.5f, 6.7f); + AZ::TransformBus::Event(m_entityIds[0], &AZ::TransformBus::Events::SetWorldTranslation, initialUnsnappedPosition); + + AzToolsFramework::SelectEntities(m_entityIds); + + EditorTransformComponentSelectionRequestBus::Event( + GetEntityContextId(), &EditorTransformComponentSelectionRequestBus::Events::SnapSelectedEntitiesToWorldGrid, 1.0f); + + const auto entityPositionAfterSnap = GetWorldTranslation(m_entity1); + const AZ::Vector3 manipulatorPositionAfterSnap = + GetManipulatorTransform().value_or(AZ::Transform::CreateIdentity()).GetTranslation(); + + const auto expectedSnappedPosition = AZ::Vector3(1.0f, 4.0f, 7.0f); + EXPECT_THAT(entityPositionAfterSnap, IsClose(expectedSnappedPosition)); + EXPECT_THAT(expectedSnappedPosition, IsClose(manipulatorPositionAfterSnap)); + } + // struct to contain input reference frame and expected orientation outcome based on // the reference frame, selection and entity hierarchy struct ReferenceFrameWithOrientation @@ -541,19 +564,20 @@ namespace UnitTest class EditorTransformComponentSelectionSingleEntityPivotFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P(EditorTransformComponentSelectionSingleEntityPivotFixture, PivotOrientationMatchesReferenceFrameSingleEntity) { - using ETCS::PivotOrientationResult; using ETCS::CalculatePivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given AZ::TransformBus::Event( m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateFromQuaternionAndTranslation( - ChildExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); + AZ::Transform::CreateFromQuaternionAndTranslation(ChildExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -574,20 +598,20 @@ namespace UnitTest All, EditorTransformComponentSelectionSingleEntityPivotFixture, testing::Values( - ReferenceFrameWithOrientation{ReferenceFrame::Local, ChildExpectedPivotLocalOrientationInWorldSpace}, - ReferenceFrameWithOrientation{ReferenceFrame::Parent, AZ::Quaternion::CreateIdentity()}, - ReferenceFrameWithOrientation{ReferenceFrame::World, AZ::Quaternion::CreateIdentity()})); + ReferenceFrameWithOrientation{ ReferenceFrame::Local, ChildExpectedPivotLocalOrientationInWorldSpace }, + ReferenceFrameWithOrientation{ ReferenceFrame::Parent, AZ::Quaternion::CreateIdentity() }, + ReferenceFrameWithOrientation{ ReferenceFrame::World, AZ::Quaternion::CreateIdentity() })); class EditorTransformComponentSelectionSingleEntityWithParentPivotFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; - TEST_P( - EditorTransformComponentSelectionSingleEntityWithParentPivotFixture, - PivotOrientationMatchesReferenceFrameEntityWithParent) + TEST_P(EditorTransformComponentSelectionSingleEntityWithParentPivotFixture, PivotOrientationMatchesReferenceFrameEntityWithParent) { - using ETCS::PivotOrientationResult; using ETCS::CalculatePivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -596,8 +620,7 @@ namespace UnitTest AZ::TransformBus::Event( parentEntityId, &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateFromQuaternionAndTranslation( - ParentExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); + AZ::Transform::CreateFromQuaternionAndTranslation(ParentExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); AZ::TransformBus::Event( m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, @@ -624,20 +647,20 @@ namespace UnitTest All, EditorTransformComponentSelectionSingleEntityWithParentPivotFixture, testing::Values( - ReferenceFrameWithOrientation{ReferenceFrame::Local, ChildExpectedPivotLocalOrientationInWorldSpace}, - ReferenceFrameWithOrientation{ReferenceFrame::Parent, ParentExpectedPivotLocalOrientationInWorldSpace}, - ReferenceFrameWithOrientation{ReferenceFrame::World, AZ::Quaternion::CreateIdentity()})); + ReferenceFrameWithOrientation{ ReferenceFrame::Local, ChildExpectedPivotLocalOrientationInWorldSpace }, + ReferenceFrameWithOrientation{ ReferenceFrame::Parent, ParentExpectedPivotLocalOrientationInWorldSpace }, + ReferenceFrameWithOrientation{ ReferenceFrame::World, AZ::Quaternion::CreateIdentity() })); class EditorTransformComponentSelectionMultipleEntitiesPivotFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; - TEST_P( - EditorTransformComponentSelectionMultipleEntitiesPivotFixture, - PivotOrientationMatchesReferenceFrameMultipleEntities) + TEST_P(EditorTransformComponentSelectionMultipleEntitiesPivotFixture, PivotOrientationMatchesReferenceFrameMultipleEntities) { - using ETCS::PivotOrientationResult; using ETCS::CalculatePivotOrientationForEntityIds; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -646,23 +669,18 @@ namespace UnitTest // setup entities in arbitrary triangle arrangement AZ::TransformBus::Event( - m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); + m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); // note: EntityIdManipulatorLookup{} is unused during this test - EntityIdManipulatorLookups lookups { - {m_entityIds[0], EntityIdManipulatorLookup{}}, - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[0], EntityIdManipulatorLookup{} }, + { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -691,14 +709,16 @@ namespace UnitTest class EditorTransformComponentSelectionMultipleEntitiesWithSameParentPivotFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionMultipleEntitiesWithSameParentPivotFixture, PivotOrientationMatchesReferenceFrameMultipleEntitiesSameParent) { - using ETCS::PivotOrientationResult; using ETCS::CalculatePivotOrientationForEntityIds; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -711,22 +731,18 @@ namespace UnitTest ParentExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateAxisZ(-5.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); AZ::TransformBus::Event(m_entityIds[1], &AZ::TransformBus::Events::SetParent, m_entityIds[0]); AZ::TransformBus::Event(m_entityIds[2], &AZ::TransformBus::Events::SetParent, m_entityIds[0]); // note: EntityIdManipulatorLookup{} is unused during this test // only select second two entities that are children of m_entityIds[0] - EntityIdManipulatorLookups lookups{ - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -755,14 +771,16 @@ namespace UnitTest class EditorTransformComponentSelectionMultipleEntitiesWithDifferentParentPivotFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionMultipleEntitiesWithDifferentParentPivotFixture, PivotOrientationMatchesReferenceFrameMultipleEntitiesDifferentParent) { - using ETCS::PivotOrientationResult; using ETCS::CalculatePivotOrientationForEntityIds; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -776,22 +794,18 @@ namespace UnitTest ParentExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateAxisZ(-5.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); AZ::TransformBus::Event(m_entityIds[1], &AZ::TransformBus::Events::SetParent, m_entityIds[0]); AZ::TransformBus::Event(m_entityIds[2], &AZ::TransformBus::Events::SetParent, m_entityIds[3]); // note: EntityIdManipulatorLookup{} is unused during this test // only select second two entities that are children of different m_entities - EntityIdManipulatorLookups lookups{ - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -819,30 +833,29 @@ namespace UnitTest class EditorTransformComponentSelectionSingleEntityPivotAndOverrideFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionSingleEntityPivotAndOverrideFixture, PivotOrientationMatchesReferenceFrameSingleEntityOptionalOverride) { - using ETCS::PivotOrientationResult; using ETCS::CalculateSelectionPivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given AZ::TransformBus::Event( m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateFromQuaternionAndTranslation( - ChildExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); + AZ::Transform::CreateFromQuaternionAndTranslation(ChildExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateZero())); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // When const ReferenceFrameWithOrientation referenceFrameWithOrientation = GetParam(); - EntityIdManipulatorLookups lookups{ - {m_entityIds[0], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[0], EntityIdManipulatorLookup{} } }; // set override frame (orientation only) OptionalFrame optionalFrame; @@ -870,14 +883,16 @@ namespace UnitTest class EditorTransformComponentSelectionMultipleEntitiesPivotAndOverrideFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionMultipleEntitiesPivotAndOverrideFixture, PivotOrientationMatchesReferenceFrameMultipleEntitiesOptionalOverride) { - using ETCS::PivotOrientationResult; using ETCS::CalculateSelectionPivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -885,23 +900,18 @@ namespace UnitTest m_entityIds.push_back(CreateDefaultEditorEntity("Entity3")); AZ::TransformBus::Event( - m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); + m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); // note: EntityIdManipulatorLookup{} is unused during this test - EntityIdManipulatorLookups lookups{ - {m_entityIds[0], EntityIdManipulatorLookup{}}, - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[0], EntityIdManipulatorLookup{} }, + { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -932,14 +942,16 @@ namespace UnitTest class EditorTransformComponentSelectionMultipleEntitiesPivotAndNoOverrideFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionMultipleEntitiesPivotAndNoOverrideFixture, PivotOrientationMatchesReferenceFrameMultipleEntitiesNoOptionalOverride) { - using ETCS::PivotOrientationResult; using ETCS::CalculateSelectionPivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -947,23 +959,18 @@ namespace UnitTest m_entityIds.push_back(CreateDefaultEditorEntity("Entity3")); AZ::TransformBus::Event( - m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); + m_entityIds[0], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(-10.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); // note: EntityIdManipulatorLookup{} is unused during this test - EntityIdManipulatorLookups lookups{ - {m_entityIds[0], EntityIdManipulatorLookup{}}, - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[0], EntityIdManipulatorLookup{} }, + { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -992,14 +999,16 @@ namespace UnitTest class EditorTransformComponentSelectionMultipleEntitiesSameParentPivotAndNoOverrideFixture : public EditorTransformComponentSelectionFixture - , public ::testing::WithParamInterface {}; + , public ::testing::WithParamInterface + { + }; TEST_P( EditorTransformComponentSelectionMultipleEntitiesSameParentPivotAndNoOverrideFixture, PivotOrientationMatchesReferenceFrameMultipleEntitiesSameParentNoOptionalOverride) { - using ETCS::PivotOrientationResult; using ETCS::CalculateSelectionPivotOrientation; + using ETCS::PivotOrientationResult; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Given @@ -1012,21 +1021,17 @@ namespace UnitTest ParentExpectedPivotLocalOrientationInWorldSpace, AZ::Vector3::CreateAxisZ(-5.0f))); AZ::TransformBus::Event( - m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); + m_entityIds[1], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisX(10.0f))); AZ::TransformBus::Event( - m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, - AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); + m_entityIds[2], &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f))); AZ::TransformBus::Event(m_entityIds[1], &AZ::TransformBus::Events::SetParent, m_entityIds[0]); AZ::TransformBus::Event(m_entityIds[2], &AZ::TransformBus::Events::SetParent, m_entityIds[0]); // note: EntityIdManipulatorLookup{} is unused during this test - EntityIdManipulatorLookups lookups{ - {m_entityIds[1], EntityIdManipulatorLookup{}}, - {m_entityIds[2], EntityIdManipulatorLookup{}} - }; + EntityIdManipulatorLookups lookups{ { m_entityIds[1], EntityIdManipulatorLookup{} }, + { m_entityIds[2], EntityIdManipulatorLookup{} } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1174,13 +1179,13 @@ namespace UnitTest AZ::TransformBus::Event(f, &AZ::TransformBus::Events::SetParent, secondLayerId); // Layer1 - // A - // B - // C - // Layer2 - // D - // E - // F + // A + // B + // C + // Layer2 + // D + // E + // F /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1270,13 +1275,13 @@ namespace UnitTest AZ::TransformBus::Event(f, &AZ::TransformBus::Events::SetParent, secondLayerId); // Layer1 - // A - // B - // C - // Layer2 - // D - // E - // F + // A + // B + // C + // Layer2 + // D + // E + // F /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1368,8 +1373,7 @@ namespace UnitTest EXPECT_TRUE(!IsEntityVisible(m_layerId)); bool flagSetVisible = false; - EditorVisibilityRequestBus::EventResult( - flagSetVisible, m_layerId, &EditorVisibilityRequestBus::Events::GetVisibilityFlag); + EditorVisibilityRequestBus::EventResult(flagSetVisible, m_layerId, &EditorVisibilityRequestBus::Events::GetVisibilityFlag); // even though a layer is set to not be visible, this is recorded by SetLayerChildrenVisibility // and AreLayerChildrenVisible - the visibility flag will not be modified and remains true @@ -1377,12 +1381,12 @@ namespace UnitTest /////////////////////////////////////////////////////////////////////////////////////////////////////////////// } - class EditorEntityInfoRequestActivateTestComponent - : public AzToolsFramework::Components::EditorComponentBase + class EditorEntityInfoRequestActivateTestComponent : public AzToolsFramework::Components::EditorComponentBase { public: AZ_EDITOR_COMPONENT( - EditorEntityInfoRequestActivateTestComponent, "{849DA1FC-6A0C-4CB8-A0BB-D90DEE7FF7F7}", + EditorEntityInfoRequestActivateTestComponent, + "{849DA1FC-6A0C-4CB8-A0BB-D90DEE7FF7F7}", AzToolsFramework::Components::EditorComponentBase); static void Reflect(AZ::ReflectContext* context); @@ -1391,13 +1395,13 @@ namespace UnitTest void Activate() override { // ensure we can successfully read IsVisible and IsLocked (bus will be connected to in entity Init) - EditorEntityInfoRequestBus::EventResult( - m_visible, GetEntityId(), &EditorEntityInfoRequestBus::Events::IsVisible); - EditorEntityInfoRequestBus::EventResult( - m_locked, GetEntityId(), &EditorEntityInfoRequestBus::Events::IsLocked); + EditorEntityInfoRequestBus::EventResult(m_visible, GetEntityId(), &EditorEntityInfoRequestBus::Events::IsVisible); + EditorEntityInfoRequestBus::EventResult(m_locked, GetEntityId(), &EditorEntityInfoRequestBus::Events::IsLocked); } - void Deactivate() override {} + void Deactivate() override + { + } bool m_visible = false; bool m_locked = true; @@ -1407,14 +1411,11 @@ namespace UnitTest { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() - ->Version(0) - ; + serializeContext->Class()->Version(0); } } - class EditorEntityModelEntityInfoRequestFixture - : public ToolsApplicationFixture + class EditorEntityModelEntityInfoRequestFixture : public ToolsApplicationFixture { public: void SetUpEditorFixtureImpl() override @@ -1435,8 +1436,7 @@ namespace UnitTest // This is necessary to prevent a warning in the undo system. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast( - &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, - entity->GetId()); + &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entity->GetId()); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1469,8 +1469,7 @@ namespace UnitTest // This is necessary to prevent a warning in the undo system. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast( - &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, - entity->GetId()); + &AzToolsFramework::ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entity->GetId()); /////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabUpdateWithPatchesTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabUpdateWithPatchesTests.cpp index aef3cad5f3..e3460c307f 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabUpdateWithPatchesTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabUpdateWithPatchesTests.cpp @@ -93,7 +93,7 @@ namespace UnitTest // Retrieve the entity pointer from the component application bus. AZ::Entity* wheelEntityUnderAxle = nullptr; - axleInstance->GetNestedEntities([&wheelEntityUnderAxle, wheelEntityIdUnderAxle](AZStd::unique_ptr& entity) + axleInstance->GetAllEntitiesInHierarchy([&wheelEntityUnderAxle, wheelEntityIdUnderAxle](AZStd::unique_ptr& entity) { if (entity->GetId() == wheelEntityIdUnderAxle) { diff --git a/Code/Framework/GridMate/GridMate/Replica/Interest/BvDynamicTree.cpp b/Code/Framework/GridMate/GridMate/Replica/Interest/BvDynamicTree.cpp deleted file mode 100644 index e72bd9b12c..0000000000 --- a/Code/Framework/GridMate/GridMate/Replica/Interest/BvDynamicTree.cpp +++ /dev/null @@ -1,1277 +0,0 @@ -/* -Bullet Continuous Collision Detection and Physics Library -Copyright (c) 2003-2006 Erwin Coumans http://continuousphysics.com/Bullet/ - -This software is provided 'as-is', without any express or implied warranty. -In no event will the authors be held liable for any damages arising from the use of this software. -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it freely, -subject to the following restrictions: - -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. -*/ - -// Modifications copyright Amazon.com, Inc. or its affiliates. - -///BvDynamicTree implementation by Nathanael Presson -#include - -namespace GridMate -{ - // - struct btDbvtNodeEnumerator : BvDynamicTree::ICollideCollector - { - BvDynamicTree::ConstNodeArrayType nodes; - void Process(const BvDynamicTree::NodeType* n) { nodes.push_back(n); } - }; - - // - static AZ_FORCE_INLINE int indexof(const BvDynamicTree::NodeType* node) - { - return (node->m_parent->m_childs[1]==node); - } - - // - static AZ_FORCE_INLINE BvDynamicTree::VolumeType merge( const BvDynamicTree::VolumeType& a, const BvDynamicTree::VolumeType& b) - { - BvDynamicTree::VolumeType res; - Merge(a,b,res); - return res; - } - - // volume+edge lengths - static AZ_FORCE_INLINE float size(const BvDynamicTree::VolumeType& a) - { - const AZ::Vector3 edges = a.GetExtents(); - return edges.GetX()*edges.GetY()*edges.GetZ() + edges.Dot(AZ::Vector3::CreateOne()); - } - - // - static void getmaxdepth(const BvDynamicTree::NodeType* node,int depth,int& maxdepth) - { - if(node->IsInternal()) - { - getmaxdepth(node->m_childs[0],depth+1,maxdepth); - getmaxdepth(node->m_childs[0],depth+1,maxdepth); - } - else - maxdepth= AZ::GetMax(maxdepth,depth); - } - - - - //========================================================================= - // insertleaf - // [3/4/2009] - //========================================================================= - void - BvDynamicTree::insertleaf( NodeType* root, NodeType* leaf) - { - if(!m_root) - { - m_root = leaf; - leaf->m_parent = 0; - } - else - { - if(!root->IsLeaf()) - { - do { - root=root->m_childs[Select( leaf->m_volume, - root->m_childs[0]->m_volume, - root->m_childs[1]->m_volume)]; - } while(!root->IsLeaf()); - } - NodeType* prev = root->m_parent; - NodeType* node = createnode(prev,leaf->m_volume,root->m_volume,0); - if(prev) - { - prev->m_childs[indexof(root)] = node; - node->m_childs[0] = root;root->m_parent=node; - node->m_childs[1] = leaf;leaf->m_parent=node; - do { - if(!prev->m_volume.Contains(node->m_volume)) - Merge(prev->m_childs[0]->m_volume,prev->m_childs[1]->m_volume,prev->m_volume); - else - break; - node=prev; - } while(0!=(prev=node->m_parent)); - } - else - { - node->m_childs[0] = root;root->m_parent=node; - node->m_childs[1] = leaf;leaf->m_parent=node; - m_root = node; - } - } - } - - //========================================================================= - // removeleaf - // [3/4/2009] - //========================================================================= - BvDynamicTree::NodeType* - BvDynamicTree::removeleaf( NodeType* leaf) - { - if(leaf==m_root) - { - m_root=0; - return 0; - } - else - { - NodeType* parent=leaf->m_parent; - NodeType* prev=parent->m_parent; - NodeType* sibling=parent->m_childs[1-indexof(leaf)]; - if(prev) - { - prev->m_childs[indexof(parent)]=sibling; - sibling->m_parent=prev; - deletenode(parent); - while(prev) - { - const VolumeType pb=prev->m_volume; - Merge(prev->m_childs[0]->m_volume,prev->m_childs[1]->m_volume,prev->m_volume); - if(NotEqual(pb,prev->m_volume)) - { - prev=prev->m_parent; - } else break; - } - return prev?prev:m_root; - } - else - { - m_root=sibling; - sibling->m_parent=0; - deletenode(parent); - return m_root; - } - } - } - - - //========================================================================= - // fetchleaves - // [3/4/2009] - //========================================================================= - void - BvDynamicTree::fetchleaves(NodeType* root,NodeArrayType& leaves,int depth) - { - if(root->IsInternal()&&depth) - { - fetchleaves(root->m_childs[0],leaves,depth-1); - fetchleaves(root->m_childs[1],leaves,depth-1); - deletenode(root); - } - else - { - leaves.push_back(root); - } - } - - //========================================================================= - // split - // [3/4/2009] - //========================================================================= - void - BvDynamicTree::split(const NodeArrayType& leaves, NodeArrayType& left, NodeArrayType& right, const AZ::Vector3& org, const AZ::Vector3& axis) - { - left.resize(0); - right.resize(0); - for(size_t i = 0, ni = leaves.size(); i < ni; ++i) - { - if (axis.Dot(leaves[i]->m_volume.GetCenter() - org) < 0.0f) - { - left.push_back(leaves[i]); - } - else - { - right.push_back(leaves[i]); - } - } - } - - //========================================================================= - // bounds - // [3/4/2009] - //========================================================================= - BvDynamicTree::VolumeType - BvDynamicTree::bounds(const NodeArrayType& leaves) - { - VolumeType volume=leaves[0]->m_volume; - for(size_t i=1,ni=leaves.size();im_volume,volume); - } - return volume; - } - - //========================================================================= - // bottomup - // [3/4/2009] - //========================================================================= - void - BvDynamicTree::bottomup( NodeArrayType& leaves ) - { - while(leaves.size()>1) - { - float minsize = std::numeric_limits::max(); - int minidx[2]={-1,-1}; - for(unsigned int i=0;im_volume,leaves[j]->m_volume)); - if(szm_volume,n[1]->m_volume,0); - p->m_childs[0] = n[0]; - p->m_childs[1] = n[1]; - n[0]->m_parent = p; - n[1]->m_parent = p; - leaves[minidx[0]] = p; - //leaves.swap(minidx[1],leaves.size()-1); - leaves[minidx[1]] = leaves.back(); - leaves.pop_back(); - } - } - - //========================================================================= - // topdown - // [3/4/2009] - //========================================================================= - BvDynamicTree::NodeType* - BvDynamicTree::topdown(NodeArrayType& leaves,int bu_treshold) - { - static const AZ::Vector3 axis[]= { AZ::Vector3(1.0f,0.0f,0.0f), AZ::Vector3(0.0f,1.0f,0.0f), AZ::Vector3(0.0f,0.0f,1.0f)}; - if(leaves.size()>1) - { - if(leaves.size()>(unsigned int)bu_treshold) - { - const VolumeType vol=bounds(leaves); - const AZ::Vector3 org=vol.GetCenter(); - NodeArrayType sets[2]; - int bestaxis=-1; - int bestmidp=(int)leaves.size(); - int splitcount[3][2]={{0,0},{0,0},{0,0}}; - - for(unsigned int i=0;im_volume.GetCenter()-org; - for(int j=0;j<3;++j) - { - ++splitcount[j][x.Dot(axis[j]) > 0.0f ? 1 : 0]; - } - } - for(unsigned int i=0;i<3;++i) - { - if((splitcount[i][0]>0)&&(splitcount[i][1]>0)) - { - // todo just remove the sign bit... - const int midp = (int)fabsf((float)(splitcount[i][0]-splitcount[i][1])); - if(midp=0) - { - sets[0].reserve(splitcount[bestaxis][0]); - sets[1].reserve(splitcount[bestaxis][1]); - split(leaves,sets[0],sets[1],org,axis[bestaxis]); - } - else - { - sets[0].reserve(leaves.size()/2+1); - sets[1].reserve(leaves.size()/2); - for(size_t i=0,ni=leaves.size();im_childs[0] = topdown(sets[0],bu_treshold); - node->m_childs[1] = topdown(sets[1],bu_treshold); - node->m_childs[0]->m_parent=node; - node->m_childs[1]->m_parent=node; - return(node); - } - else - { - bottomup(leaves); - return(leaves[0]); - } - } - return(leaves[0]); - } - - //========================================================================= - // sort - // [3/4/2009] - //========================================================================= - AZ_FORCE_INLINE BvDynamicTree::NodeType* - BvDynamicTree::sort(NodeType* n,NodeType*& r) - { - BvDynamicTree::NodeType* p=n->m_parent; - AZ_Assert(n->IsInternal(), "We can call this only for internal nodes!"); - if(p>n) - { - const int i=indexof(n); - const int j=1-i; - NodeType* s=p->m_childs[j]; - NodeType* q=p->m_parent; - AZ_Assert(n==p->m_childs[i], ""); - if(q) q->m_childs[indexof(p)]=n; else r=n; - s->m_parent=n; - p->m_parent=n; - n->m_parent=q; - p->m_childs[0]=n->m_childs[0]; - p->m_childs[1]=n->m_childs[1]; - n->m_childs[0]->m_parent=p; - n->m_childs[1]->m_parent=p; - n->m_childs[i]=p; - n->m_childs[j]=s; - AZStd::swap(p->m_volume,n->m_volume); - return(p); - } - return(n); - } - - #if 0 - static DBVT_INLINE NodeType* walkup(NodeType* n,int count) - { - while(n&&(count--)) n=n->parent; - return(n); - } - #endif - - // - // Api - // - - // - BvDynamicTree::BvDynamicTree() - { - m_root = 0; - m_free = 0; - m_lkhd = -1; - m_leaves = 0; - m_opath = 0; - } - - // - BvDynamicTree::~BvDynamicTree() - { - Clear(); - } - - // - void BvDynamicTree::Clear() - { - if(m_root) recursedeletenode(m_root); - delete m_free; - m_free=0; - } - - // - void BvDynamicTree::OptimizeBottomUp() - { - if(m_root) - { - NodeArrayType leaves; - leaves.reserve(m_leaves); - fetchleaves(m_root,leaves); - bottomup(leaves); - m_root=leaves[0]; - } - } - - // - void - BvDynamicTree::OptimizeTopDown(int bu_treshold) - { - if(m_root) - { - NodeArrayType leaves; - leaves.reserve(m_leaves); - fetchleaves(m_root,leaves); - m_root=topdown(leaves,bu_treshold); - } - } - - // - void - BvDynamicTree::OptimizeIncremental(int passes) - { - if(passes<0) passes=m_leaves; - if(m_root&&(passes>0)) - { - do { - NodeType* node=m_root; - unsigned bit=0; - while(node->IsInternal()) - { - node=sort(node,m_root)->m_childs[(m_opath>>bit)&1]; - bit=(bit+1)&(sizeof(unsigned)*8-1); - } - Update(node); - ++m_opath; - } while(--passes); - } - } - - // - BvDynamicTree::NodeType* - BvDynamicTree::Insert(const VolumeType& volume,void* data) - { - NodeType* leaf=createnode(0,volume,data); - insertleaf(m_root,leaf); - ++m_leaves; - return(leaf); - } - - // - void - BvDynamicTree::Update(NodeType* leaf,int lookahead) - { - NodeType* root=removeleaf(leaf); - if(root) - { - if(lookahead>=0) - { - for(int i=0;(im_parent;++i) - { - root=root->m_parent; - } - } else root=m_root; - } - insertleaf(root,leaf); - } - - // - void - BvDynamicTree::Update(NodeType* leaf,VolumeType& volume) - { - NodeType* root=removeleaf(leaf); - if(root) - { - if(m_lkhd>=0) - { - for(int i=0;(im_parent;++i) - { - root=root->m_parent; - } - } else root=m_root; - } - leaf->m_volume=volume; - insertleaf(root,leaf); - } - - // - bool - BvDynamicTree::Update(NodeType* leaf,VolumeType& volume,const AZ::Vector3& velocity,const float margin) - { - if(leaf->m_volume.Contains(volume)) return(false); - volume.Expand(AZ::Vector3(margin)); - volume.SignedExpand(velocity); - Update(leaf,volume); - return(true); - } - - // - bool - BvDynamicTree::Update(NodeType* leaf,VolumeType& volume,const AZ::Vector3& velocity) - { - if(leaf->m_volume.Contains(volume)) return(false); - volume.SignedExpand(velocity); - Update(leaf,volume); - return(true); - } - - // - bool - BvDynamicTree::Update(NodeType* leaf,VolumeType& volume,const float margin) - { - if(leaf->m_volume.Contains(volume)) return(false); - volume.Expand(AZ::Vector3(margin)); - Update(leaf,volume); - return(true); - } - - // - void - BvDynamicTree::Remove(NodeType* leaf) - { - removeleaf(leaf); - deletenode(leaf); - --m_leaves; - } - - template - int findLinearSearch(BvDynamicTree::ConstNodeArrayType& arr, const T& key) - { - size_t numElements = arr.size(); - int index = (int)numElements; - - for(size_t i=0;iPrepare(m_root,(unsigned int)nodes.nodes.size()); - for(unsigned int i=0;i<(unsigned int)nodes.nodes.size();++i) - { - const NodeType* n=nodes.nodes[i]; - int p=-1; - if(n->m_parent) p = findLinearSearch(nodes.nodes,n->m_parent); - if(n->IsInternal()) - { - const int c0=findLinearSearch(nodes.nodes,n->m_childs[0]); - const int c1=findLinearSearch(nodes.nodes,n->m_childs[1]); - iwriter->WriteNode(n,i,p,c0,c1); - } - else - { - iwriter->WriteLeaf(n,i,p); - } - } - } - - // - void - BvDynamicTree::Clone(BvDynamicTree& dest,IClone* iclone) const - { - dest.Clear(); - if(m_root!=0) - { - vector stack; - stack.reserve(m_leaves); - stack.push_back(sStkCLN(m_root,0)); - do { - const size_t i=stack.size()-1; - const sStkCLN e=stack[i]; - NodeType* n= dest.createnode(e.parent,e.node->m_volume,e.node->m_data); - stack.pop_back(); - if(e.parent!=0) - e.parent->m_childs[i&1]=n; - else - dest.m_root=n; - if(e.node->IsInternal()) - { - stack.push_back(sStkCLN(e.node->m_childs[0],n)); - stack.push_back(sStkCLN(e.node->m_childs[1],n)); - } - else - { - iclone->CloneLeaf(n); - } - } while(!stack.empty()); - } - } - - // - int - BvDynamicTree::GetMaxDepth(const NodeType* node) - { - int depth=0; - if(node) getmaxdepth(node,1,depth); - return depth ; - } - - // - int - BvDynamicTree::CountLeaves(const NodeType* node) - { - if(node->IsInternal()) - return(CountLeaves(node->m_childs[0])+CountLeaves(node->m_childs[1])); - else - return(1); - } - - // - void - BvDynamicTree::ExtractLeaves(const NodeType* node,vector& leaves) - { - if(node->IsInternal()) - { - ExtractLeaves(node->m_childs[0],leaves); - ExtractLeaves(node->m_childs[1],leaves); - } - else - { - leaves.push_back(node); - } - } - - // - #if DBVT_ENABLE_BENCHMARK - - #include - #include - #include - - /* - q6600,2.4ghz - - /Ox /Ob2 /Oi /Ot /I "." /I "..\.." /I "..\..\src" /D "NDEBUG" /D "_LIB" /D "_WINDOWS" /D "_CRT_SECURE_NO_DEPRECATE" /D "_CRT_NONSTDC_NO_DEPRECATE" /D "WIN32" - /GF /FD /MT /GS- /Gy /arch:SSE2 /Zc:wchar_t- /Fp"..\..\out\release8\build\libbulletcollision\libbulletcollision.pch" - /Fo"..\..\out\release8\build\libbulletcollision\\" - /Fd"..\..\out\release8\build\libbulletcollision\bulletcollision.pdb" - /W3 /nologo /c /Wp64 /Zi /errorReport:prompt - - Benchmarking dbvt... - World scale: 100.000000 - Extents base: 1.000000 - Extents range: 4.000000 - Leaves: 8192 - sizeof(VolumeType): 32 bytes - sizeof(NodeType): 44 bytes - [1] VolumeType intersections: 3499 ms (-1%) - [2] VolumeType merges: 1934 ms (0%) - [3] BvDynamicTree::collideTT: 5485 ms (-21%) - [4] BvDynamicTree::collideTT self: 2814 ms (-20%) - [5] BvDynamicTree::collideTT xform: 7379 ms (-1%) - [6] BvDynamicTree::collideTT xform,self: 7270 ms (-2%) - [7] BvDynamicTree::rayTest: 6314 ms (0%),(332143 r/s) - [8] insert/remove: 2093 ms (0%),(1001983 ir/s) - [9] updates (teleport): 1879 ms (-3%),(1116100 u/s) - [10] updates (jitter): 1244 ms (-4%),(1685813 u/s) - [11] optimize (incremental): 2514 ms (0%),(1668000 o/s) - [12] VolumeType notequal: 3659 ms (0%) - [13] culling(OCL+fullsort): 2218 ms (0%),(461 t/s) - [14] culling(OCL+qsort): 3688 ms (5%),(2221 t/s) - [15] culling(KDOP+qsort): 1139 ms (-1%),(7192 t/s) - [16] insert/remove batch(256): 5092 ms (0%),(823704 bir/s) - [17] VolumeType select: 3419 ms (0%) - */ - - struct btDbvtBenchmark - { - struct NilPolicy : BvDynamicTree::ICollide - { - NilPolicy() : m_pcount(0),m_depth(-SIMD_INFINITY),m_checksort(true) {} - void Process(const NodeType*,const NodeType*) { ++m_pcount; } - void Process(const NodeType*) { ++m_pcount; } - void Process(const NodeType*,btScalar depth) - { - ++m_pcount; - if(m_checksort) - { if(depth>=m_depth) m_depth=depth; else printf("wrong depth: %f (should be >= %f)\r\n",depth,m_depth); } - } - int m_pcount; - btScalar m_depth; - bool m_checksort; - }; - struct P14 : BvDynamicTree::ICollide - { - struct Node - { - const NodeType* leaf; - btScalar depth; - }; - void Process(const NodeType* leaf,btScalar depth) - { - Node n; - n.leaf = leaf; - n.depth = depth; - } - static int sortfnc(const Node& a,const Node& b) - { - if(a.depthb.depth) return(-1); - return(0); - } - btAlignedObjectArray m_nodes; - }; - struct P15 : BvDynamicTree::ICollide - { - struct Node - { - const NodeType* leaf; - btScalar depth; - }; - void Process(const NodeType* leaf) - { - Node n; - n.leaf = leaf; - n.depth = dot(leaf->volume.GetCenter(),m_axis); - } - static int sortfnc(const Node& a,const Node& b) - { - if(a.depthb.depth) return(-1); - return(0); - } - btAlignedObjectArray m_nodes; - btAZ::Vector3 m_axis; - }; - static btScalar RandUnit() - { - return(rand()/(btScalar)RAND_MAX); - } - static btAZ::Vector3 RandAZ::Vector3() - { - return(btAZ::Vector3(RandUnit(),RandUnit(),RandUnit())); - } - static btAZ::Vector3 RandAZ::Vector3(btScalar cs) - { - return(RandAZ::Vector3()*cs-btAZ::Vector3(cs,cs,cs)/2); - } - static VolumeType RandVolume(btScalar cs,btScalar eb,btScalar es) - { - return(VolumeType::FromCE(RandAZ::Vector3(cs),btAZ::Vector3(eb,eb,eb)+RandAZ::Vector3()*es)); - } - static btTransform RandTransform(btScalar cs) - { - btTransform t; - t.setOrigin(RandAZ::Vector3(cs)); - t.setRotation(btQuaternion(RandUnit()*SIMD_PI*2,RandUnit()*SIMD_PI*2,RandUnit()*SIMD_PI*2).normalized()); - return(t); - } - static void RandTree(btScalar cs,btScalar eb,btScalar es,int leaves,BvDynamicTree& dbvt) - { - dbvt.clear(); - for(int i=0;i volumes; - btAlignedObjectArray results; - volumes.resize(cfgLeaves); - results.resize(cfgLeaves); - for(int i=0;i volumes; - btAlignedObjectArray results; - volumes.resize(cfgLeaves); - results.resize(cfgLeaves); - for(int i=0;i transforms; - btDbvtBenchmark::NilPolicy policy; - transforms.resize(cfgBenchmark5_Iterations); - for(int i=0;i transforms; - btDbvtBenchmark::NilPolicy policy; - transforms.resize(cfgBenchmark6_Iterations); - for(int i=0;i rayorg; - btAlignedObjectArray raydir; - btDbvtBenchmark::NilPolicy policy; - rayorg.resize(cfgBenchmark7_Iterations); - raydir.resize(cfgBenchmark7_Iterations); - for(int i=0;i leaves; - btDbvtBenchmark::RandTree(cfgVolumeCenterScale,cfgVolumeExentsBase,cfgVolumeExentsScale,cfgLeaves,dbvt); - dbvt.optimizeTopDown(); - dbvt.extractLeaves(dbvt.m_root,leaves); - printf("[9] updates (teleport): "); - wallclock.reset(); - for(int i=0;i(leaves[rand()%cfgLeaves]), - btDbvtBenchmark::RandVolume(cfgVolumeCenterScale,cfgVolumeExentsBase,cfgVolumeExentsScale)); - } - } - const int time=(int)wallclock.getTimeMilliseconds(); - const int up=cfgBenchmark9_Passes*cfgBenchmark9_Iterations; - printf("%u ms (%i%%),(%u u/s)\r\n",time,(time-cfgBenchmark9_Reference)*100/time,up*1000/time); - } - if(cfgBenchmark10_Enable) - {// Benchmark 10 - srand(380843); - BvDynamicTree dbvt; - btAlignedObjectArray leaves; - btAlignedObjectArray vectors; - vectors.resize(cfgBenchmark10_Iterations); - for(int i=0;i(leaves[rand()%cfgLeaves]); - VolumeType v=VolumeType::FromMM(l->volume.GetMin()+d,l->volume.GetMax()+d); - dbvt.update(l,v); - } - } - const int time=(int)wallclock.getTimeMilliseconds(); - const int up=cfgBenchmark10_Passes*cfgBenchmark10_Iterations; - printf("%u ms (%i%%),(%u u/s)\r\n",time,(time-cfgBenchmark10_Reference)*100/time,up*1000/time); - } - if(cfgBenchmark11_Enable) - {// Benchmark 11 - srand(380843); - BvDynamicTree dbvt; - btDbvtBenchmark::RandTree(cfgVolumeCenterScale,cfgVolumeExentsBase,cfgVolumeExentsScale,cfgLeaves,dbvt); - dbvt.optimizeTopDown(); - printf("[11] optimize (incremental): "); - wallclock.reset(); - for(int i=0;i volumes; - btAlignedObjectArray results; - volumes.resize(cfgLeaves); - results.resize(cfgLeaves); - for(int i=0;i vectors; - btDbvtBenchmark::NilPolicy policy; - vectors.resize(cfgBenchmark13_Iterations); - for(int i=0;i vectors; - btDbvtBenchmark::P14 policy; - vectors.resize(cfgBenchmark14_Iterations); - for(int i=0;i vectors; - btDbvtBenchmark::P15 policy; - vectors.resize(cfgBenchmark15_Iterations); - for(int i=0;i batch; - btDbvtBenchmark::RandTree(cfgVolumeCenterScale,cfgVolumeExentsBase,cfgVolumeExentsScale,cfgLeaves,dbvt); - dbvt.optimizeTopDown(); - batch.reserve(cfgBenchmark16_BatchCount); - printf("[16] Insert/remove batch(%u): ",cfgBenchmark16_BatchCount); - wallclock.reset(); - for(int i=0;i volumes; - btAlignedObjectArray results; - btAlignedObjectArray indices; - volumes.resize(cfgLeaves); - results.resize(cfgLeaves); - indices.resize(cfgLeaves); - for(int i=0;i -#include -#include - -#include -#include - -namespace GridMate -{ - namespace Internal - { - /** - * - */ - class DynamicTreeAabb : public AZ::Aabb - { - public: - GM_CLASS_ALLOCATOR(DynamicTreeAabb); - - AZ_FORCE_INLINE explicit DynamicTreeAabb() {} - AZ_FORCE_INLINE DynamicTreeAabb(const AZ::Aabb& aabb) : AZ::Aabb(aabb) {} - AZ_FORCE_INLINE explicit DynamicTreeAabb(const AZ::Vector3& min,const AZ::Vector3& max) : AZ::Aabb(AZ::Aabb::CreateFromMinMax(min,max)) {} - - AZ_FORCE_INLINE static DynamicTreeAabb CreateFromFacePoints(const AZ::Vector3& a, const AZ::Vector3& b, const AZ::Vector3& c) - { - DynamicTreeAabb vol(a,a); - vol.AddPoint(b); - vol.AddPoint(c); - return vol; - } - - AZ_FORCE_INLINE void SignedExpand(const AZ::Vector3& e) - { - AZ::Vector3 zero = AZ::Vector3::CreateZero(); - AZ::Vector3 mxE = m_max + e; - AZ::Vector3 miE = m_min + e; - m_max = AZ::Vector3::CreateSelectCmpGreater(e,zero,mxE,m_max ); - m_min = AZ::Vector3::CreateSelectCmpGreater(e,zero,m_min,miE); - } - AZ_FORCE_INLINE int Classify(const AZ::Vector3& n,const float o,int s) const - { - AZ::Vector3 pi, px; - switch(s) - { - case (0+0+0): px=m_min; - pi=m_max; break; - case (1+0+0): px=AZ::Vector3(m_max.GetX(),m_min.GetY(),m_min.GetZ()); - pi=AZ::Vector3(m_min.GetX(),m_max.GetY(),m_max.GetZ());break; - case (0+2+0): px=AZ::Vector3(m_min.GetX(),m_max.GetY(),m_min.GetZ()); - pi=AZ::Vector3(m_max.GetX(),m_min.GetY(),m_max.GetZ());break; - case (1+2+0): px=AZ::Vector3(m_max.GetX(),m_max.GetY(),m_min.GetZ()); - pi=AZ::Vector3(m_min.GetX(),m_min.GetY(),m_max.GetZ());break; - case (0+0+4): px=AZ::Vector3(m_min.GetX(),m_min.GetY(),m_max.GetZ()); - pi=AZ::Vector3(m_max.GetX(),m_max.GetY(),m_min.GetZ());break; - case (1+0+4): px=AZ::Vector3(m_max.GetX(),m_min.GetY(),m_max.GetZ()); - pi=AZ::Vector3(m_min.GetX(),m_max.GetY(),m_min.GetZ());break; - case (0+2+4): px=AZ::Vector3(m_min.GetX(),m_max.GetY(),m_max.GetZ()); - pi=AZ::Vector3(m_max.GetX(),m_min.GetY(),m_min.GetZ());break; - case (1+2+4): px=m_max; - pi=m_min;break; - } - - if (n.Dot(px) + o < 0.0f) - { - return -1; - } - if (n.Dot(pi) + o > 0.0f) - { - return 1; - } - - return 0; - } - AZ_FORCE_INLINE float ProjectMinimum(const AZ::Vector3& v, unsigned signs) const - { - const AZ::Vector3* b[]={&m_max,&m_min}; - const AZ::Vector3 p( b[(signs>>0)&1]->GetX(),b[(signs>>1)&1]->GetY(),b[(signs>>2)&1]->GetZ()); - return p.Dot(v); - } - - // Move the code here - AZ_FORCE_INLINE friend bool IntersectAabbAabb(const DynamicTreeAabb& a,const DynamicTreeAabb& b); - AZ_FORCE_INLINE friend bool IntersectAabbPoint(const DynamicTreeAabb& a, const AZ::Vector3& b); - AZ_FORCE_INLINE friend bool IntersectAabbPlane(const DynamicTreeAabb& a, const AZ::Plane& b); - AZ_FORCE_INLINE friend float Proximity(const DynamicTreeAabb& a, const DynamicTreeAabb& b); - AZ_FORCE_INLINE friend int Select(const DynamicTreeAabb& o, const DynamicTreeAabb& a, const DynamicTreeAabb& b); - AZ_FORCE_INLINE friend void Merge(const DynamicTreeAabb& a, const DynamicTreeAabb& b, DynamicTreeAabb& r); - AZ_FORCE_INLINE friend bool NotEqual(const DynamicTreeAabb& a, const DynamicTreeAabb& b); - private: - AZ_FORCE_INLINE void AddSpan(const AZ::Vector3& d, float& smi, float& smx) const - { - AZ::Vector3 vecZero = AZ::Vector3::CreateZero(); - AZ::Vector3 mxD = m_max*d; - AZ::Vector3 miD = m_min*d; - AZ::Vector3 smiAdd = AZ::Vector3::CreateSelectCmpGreater(vecZero,d,mxD,miD); - AZ::Vector3 smxAdd = AZ::Vector3::CreateSelectCmpGreater(vecZero,d,miD,mxD); - AZ::Vector3 vecOne = AZ::Vector3::CreateOne(); - // sum components - smi += smiAdd.Dot(vecOne); - smx += smxAdd.Dot(vecOne); - } - }; - - // - AZ_FORCE_INLINE bool IntersectAabbAabb(const DynamicTreeAabb& a, const DynamicTreeAabb& b) - { - return a.Overlaps(b); - } - - AZ_FORCE_INLINE bool IntersectAabbPlane(const DynamicTreeAabb& a, const AZ::Plane& b) - { - //use plane normal to quickly select the nearest corner of the aabb - AZ::Vector3 testPoint = AZ::Vector3::CreateSelectCmpGreater(b.GetNormal(), AZ::Vector3::CreateZero(), a.GetMin(), a.GetMax()); - //test if nearest point is inside the plane - return b.GetPointDist(testPoint) <= 0.0f; - } - - // - AZ_FORCE_INLINE float Proximity(const DynamicTreeAabb& a, const DynamicTreeAabb& b) - { - const AZ::Vector3 d=(a.m_min+a.m_max)-(b.m_min+b.m_max); - // get abs and sum - return d.GetAbs().Dot(AZ::Vector3::CreateOne()); - } - - // - AZ_FORCE_INLINE int Select( const DynamicTreeAabb& o, const DynamicTreeAabb& a, const DynamicTreeAabb& b) - { - return Proximity(o,a) < Proximity(o,b); - } - - // - AZ_FORCE_INLINE void Merge(const DynamicTreeAabb& a, const DynamicTreeAabb& b, DynamicTreeAabb& r) - { - r.m_min = AZ::Vector3::CreateSelectCmpGreater(b.m_min,a.m_min,a.m_min,b.m_min); - r.m_max = AZ::Vector3::CreateSelectCmpGreater(a.m_max,b.m_max,a.m_max,b.m_max); - } - - // - AZ_FORCE_INLINE bool NotEqual( const DynamicTreeAabb& a, const DynamicTreeAabb& b) - { - return (a.m_min != b.m_min || a.m_max != b.m_max); - } - - - /* NodeType */ - struct DynamicTreeNode - { - GM_CLASS_ALLOCATOR(DynamicTreeNode); - - DynamicTreeAabb m_volume; - DynamicTreeNode* m_parent; - AZ_FORCE_INLINE bool IsLeaf() const { return(m_childs[1]==0); } - AZ_FORCE_INLINE bool IsInternal() const { return(!IsLeaf()); } - union - { - DynamicTreeNode* m_childs[2]; - void* m_data; - int m_dataAsInt; - }; - }; - } - - /** - * Implementation of dynamic aabb tree, based on the bullet dynamic tree (btDbvt). - * - * The BvDynamicTree class implements a fast dynamic bounding volume tree based on axis aligned bounding boxes (aabb tree). - * This BvDynamicTree is used for soft body collision detection and for the btDbvtBroadphase. It has a fast insert, remove and update of nodes. - * Unlike the BvTreeQuantized, nodes can be dynamically moved around, which allows for change in topology of the underlying data structure. - */ - class BvDynamicTree - { - public: - using Ptr = AZStd::intrusive_ptr; - - GM_CLASS_ALLOCATOR(BvDynamicTree); - - typedef Internal::DynamicTreeAabb VolumeType; - typedef Internal::DynamicTreeNode NodeType; - - typedef vector NodeArrayType; - typedef vector ConstNodeArrayType; - - private: - - /* Stack element */ - struct sStkNN - { - const NodeType* a; - const NodeType* b; - sStkNN() {} - sStkNN(const NodeType* na,const NodeType* nb) : a(na), b(nb) {} - }; - struct sStkNP - { - const NodeType* node; - int mask; - sStkNP(const NodeType* n, unsigned m) : node(n), mask(m) {} - }; - struct sStkNPS - { - const NodeType* node; - int mask; - float value; - sStkNPS() {} - sStkNPS(const NodeType* n, unsigned m, const float v) : node(n), mask(m), value(v) {} - }; - struct sStkCLN - { - const NodeType* node; - NodeType* parent; - sStkCLN(const NodeType* n, NodeType* p) : node(n), parent(p) {} - }; - - public: - /* ICollideCollector templated collectors should implement this functions or inherit from this class */ - struct ICollideCollector - { - void Process(const NodeType*, const NodeType*) {} - void Process(const NodeType*) {} - void Process(const NodeType* n, const float) { Process(n); } - bool Descent(const NodeType*) { return true; } - bool AllLeaves(const NodeType*) { return true; } - }; - - /* IWriter */ - struct IWriter - { - virtual ~IWriter() {} - virtual void Prepare(const NodeType* root,int numnodes) = 0; - virtual void WriteNode(const NodeType*, int index, int parent, int child0, int child1) = 0; - virtual void WriteLeaf(const NodeType*, int index, int parent) = 0; - }; - /* IClone */ - struct IClone - { - virtual ~IClone() {} - virtual void CloneLeaf(NodeType*) {} - }; - - // Constants - enum - { - SIMPLE_STACKSIZE = 64, - DOUBLE_STACKSIZE = SIMPLE_STACKSIZE * 2 - }; - - // Methods - BvDynamicTree(); - ~BvDynamicTree(); - - NodeType* GetRoot() const { return m_root; } - void Clear(); - bool Empty() const { return 0 == m_root; } - int GetNumLeaves() const { return m_leaves; } - void OptimizeBottomUp(); - void OptimizeTopDown(int bu_treshold = 128); - void OptimizeIncremental(int passes); - NodeType* Insert(const VolumeType& box,void* data); - void Update(NodeType* leaf, int lookahead=-1); - void Update(NodeType* leaf, VolumeType& volume); - bool Update(NodeType* leaf, VolumeType& volume, const AZ::Vector3& velocity, const float margin); - bool Update(NodeType* leaf, VolumeType& volume, const AZ::Vector3& velocity); - bool Update(NodeType* leaf, VolumeType& volume, const float margin); - void Remove(NodeType* leaf); - void Write(IWriter* iwriter) const; - void Clone(BvDynamicTree& dest, IClone* iclone=0) const; - static int GetMaxDepth(const NodeType* node); - static int CountLeaves(const NodeType* node); - static void ExtractLeaves(const NodeType* node, /*btAlignedObjectArray&*/vector& leaves); - #if DBVT_ENABLE_BENCHMARK - static void Benchmark(); - #else - static void Benchmark(){} - #endif - /** - * Collector should inherit from ICollide - */ - template - static inline void enumNodes( const NodeType* root, Collector& collector) - { - collector.Process(root); - if(root->IsInternal()) - { - enumNodes(root->m_childs[0],collector); - enumNodes(root->m_childs[1],collector); - } - } - template - static void enumLeaves( const NodeType* root,Collector& collector) - { - if(root->IsInternal()) - { - enumLeaves(root->m_childs[0],collector); - enumLeaves(root->m_childs[1],collector); - } - else - { - collector.Process(root); - } - } - template - void collideTT( const NodeType* root0,const NodeType* root1,Collector& collector) const - { - if(root0&&root1) - { - size_t depth=1; - size_t treshold=DOUBLE_STACKSIZE-4; - vector stkStack; - stkStack.resize(DOUBLE_STACKSIZE); - stkStack[0]=sStkNN(root0,root1); - do { - sStkNN p=stkStack[--depth]; - if(depth>treshold) - { - stkStack.resize(stkStack.size()*2); - treshold=stkStack.size()-4; - } - if(p.a==p.b) - { - if(p.a->IsInternal()) - { - stkStack[depth++]=sStkNN(p.a->m_childs[0],p.a->m_childs[0]); - stkStack[depth++]=sStkNN(p.a->m_childs[1],p.a->m_childs[1]); - stkStack[depth++]=sStkNN(p.a->m_childs[0],p.a->m_childs[1]); - } - } - else if(IntersectAabbAabb(p.a->m_volume,p.b->m_volume)) - { - if(p.a->IsInternal()) - { - if(p.b->IsInternal()) - { - stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b->m_childs[0]); - stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b->m_childs[0]); - stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b->m_childs[1]); - stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b->m_childs[1]); - } - else - { - stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b); - stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b); - } - } - else - { - if(p.b->IsInternal()) - { - stkStack[depth++]=sStkNN(p.a,p.b->m_childs[0]); - stkStack[depth++]=sStkNN(p.a,p.b->m_childs[1]); - } - else - { - collector.Process(p.a,p.b); - } - } - } - } while(depth); - } - } - template - void collideTTpersistentStack( const NodeType* root0, const NodeType* root1,Collector& collector) - { - if(root0&&root1) - { - size_t depth=1; - size_t treshold=DOUBLE_STACKSIZE-4; - - m_stkStack.resize(DOUBLE_STACKSIZE); - m_stkStack[0]=sStkNN(root0,root1); - do - { - sStkNN p=m_stkStack[--depth]; - if(depth>treshold) - { - m_stkStack.resize(m_stkStack.size()*2); - treshold=m_stkStack.size()-4; - } - if(p.a==p.b) - { - if(p.a->IsInternal()) - { - m_stkStack[depth++]=sStkNN(p.a->m_childs[0],p.a->m_childs[0]); - m_stkStack[depth++]=sStkNN(p.a->m_childs[1],p.a->m_childs[1]); - m_stkStack[depth++]=sStkNN(p.a->m_childs[0],p.a->m_childs[1]); - } - } - else if(IntersectAabbAabb(p.a->m_volume,p.b->m_volume)) - { - if(p.a->IsInternal()) - { - if(p.b->IsInternal()) - { - m_stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b->m_childs[0]); - m_stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b->m_childs[0]); - m_stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b->m_childs[1]); - m_stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b->m_childs[1]); - } - else - { - m_stkStack[depth++]=sStkNN(p.a->m_childs[0],p.b); - m_stkStack[depth++]=sStkNN(p.a->m_childs[1],p.b); - } - } - else - { - if(p.b->IsInternal()) - { - m_stkStack[depth++]=sStkNN(p.a,p.b->m_childs[0]); - m_stkStack[depth++]=sStkNN(p.a,p.b->m_childs[1]); - } - else - { - collector.Process(p.a,p.b); - } - } - } - } while(depth); - } - } - template - void collideTV( const NodeType* root, const VolumeType& volume, Collector& collector) const - { - if(root) - { -// ATTRIBUTE_ALIGNED16(VolumeType) volume(vol); -// btAlignedObjectArray stack; - AZStd::fixed_vector stack; - //stack.reserve(SIMPLE_STACKSIZE); - stack.push_back(root); - do { - const NodeType* n=stack[stack.size()-1]; - stack.pop_back(); - if(IntersectAabbAabb(n->m_volume,volume)) - { - if(n->IsInternal()) - { - stack.push_back(n->m_childs[0]); - stack.push_back(n->m_childs[1]); - } - else - { - collector.Process(n); - } - } - } while(!stack.empty()); - } - } - - template - void collideTP(const NodeType* root, const AZ::Plane& plane, Collector& collector) const - { - if (root) - { - AZStd::fixed_vector stack; - stack.push_back(root); - do - { - const NodeType* n=stack[stack.size()-1]; - stack.pop_back(); - if (IntersectAabbPlane(n->m_volume, plane)) - { - if(n->IsInternal()) - { - stack.push_back(n->m_childs[0]); - stack.push_back(n->m_childs[1]); - } - else - { - collector.Process(n); - } - } - } while (!stack.empty()); - } - } - - ///rayTest is a re-entrant ray test, and can be called in parallel as long as the btAlignedAlloc is thread-safe (uses locking etc) - ///rayTest is slower than rayTestInternal, because it builds a local stack, using memory allocations, and it recomputes signs/rayDirectionInverses each time - template - static void rayTest( const NodeType* root, const AZ::Vector3& rayFrom, const AZ::Vector3& rayTo, Collector& collector) - { - if(root) - { - AZ::Vector3 ray = rayTo-rayFrom; - AZ::Vector3 rayDir = ray.GetNormalized(); - - ///what about division by zero? --> just set rayDirection[i] to INF/1e30 - AZ::Vector3 rayDirectionInverse = AZ::Vector3::CreateSelectCmpEqual(rayDir,AZ::Vector3::CreateZero(),AZ::Vector3(1e30),rayDir.GetReciprocal()); - - unsigned int signs[3];// = { rayDirectionInverse[0] < 0.0f, rayDirectionInverse[1] < 0.0f, rayDirectionInverse[2] < 0.0f }; - signs[0] = rayDirectionInverse.GetX() < 0.0f; - signs[1] = rayDirectionInverse.GetY() < 0.0f; - signs[2] = rayDirectionInverse.GetZ() < 0.0f; - - //float lambda_max = rayDir.Dot(ray); - - AZ::Vector3 resultNormal; - - //btAlignedObjectArray stack; - vector stack; - - int depth=1; - int treshold=DOUBLE_STACKSIZE-2; - - stack.resize(DOUBLE_STACKSIZE); - stack[0]=root; - AZ::Vector3 bounds[2]; - do { - const NodeType* node=stack[--depth]; - - bounds[0] = node->m_volume.GetMin(); - bounds[1] = node->m_volume.GetMax(); - - //float tmin = 1.0f; - //float lambda_min = 0.0f; - // todo.. - unsigned int result1 = /*btRayAabb2(rayFrom,rayDirectionInverse,signs,bounds,tmin,lambda_min,lambda_max)*/0; -#ifdef COMPARE_BTRAY_AABB2 - float param = 1.0f; - bool result2 = /*btRayAabb(rayFrom,rayTo,node->volume.GetMin(),node->volume.GetMax(),param,resultNormal)*/0; - AZ_Assert(result1 == result2, ""); -#endif //TEST_BTRAY_AABB2 - if(result1) - { - if(node->IsInternal()) - { - if(depth>treshold) - { - stack.resize(stack.size()*2); - treshold=stack.size()-2; - } - stack[depth++]=node->m_childs[0]; - stack[depth++]=node->m_childs[1]; - } - else - { - collector.Process(node); - } - } - } while(depth); - - } - } - - ///rayTestInternal is faster than rayTest, because it uses a persistent stack (to reduce dynamic memory allocations to a minimum) and it uses precomputed signs/rayInverseDirections - ///rayTestInternal is used by btDbvtBroadphase to accelerate world ray casts - template - void rayTestInternal(const NodeType* root, const AZ::Vector3& rayFrom, const AZ::Vector3& rayTo, const AZ::Vector3& rayDirectionInverse, unsigned int signs[3], const float lambda_max, const AZ::Vector3& aabbMin, const AZ::Vector3& aabbMax, Collector& collector) const - { - (void)rayFrom;(void)rayTo;(void)rayDirectionInverse;(void)signs;(void)lambda_max; - if(root) - { - AZ::Vector3 resultNormal; - - int depth=1; - int treshold=DOUBLE_STACKSIZE-2; - vector stack; - stack.resize(DOUBLE_STACKSIZE); - stack[0]=root; - AZ::Vector3 bounds[2]; - do - { - const NodeType* node=stack[--depth]; - bounds[0] = node->m_volume.GetMin()+aabbMin; - bounds[1] = node->m_volume.GetMax()+aabbMax; - - //float tmin = 1.0f; - //float lambda_min = 0.0f; - unsigned int result1=false; - // todo... - result1 = /*btRayAabb2(rayFrom,rayDirectionInverse,signs,bounds,tmin,lambda_min,lambda_max)*/false; - if(result1) - { - if(node->IsInternal()) - { - if(depth>treshold) - { - stack.resize(stack.size()*2); - treshold=stack.size()-2; - } - stack[depth++]=node->m_childs[0]; - stack[depth++]=node->m_childs[1]; - } - else - { - collector.Process(node); - } - } - } while(depth); - } - } - - template - static void collideKDOP(const NodeType* root, const AZ::Vector3* normals, const float* offsets, int count, Collector& collector) - { - (void)root;(void)normals;(void)offsets;(void)count;(void)collector; -/* if(root) - { - const int inside=(1< stack; - int signs[sizeof(unsigned)*8]; - btAssert(count=0)?1:0)+ - ((normals[i].y()>=0)?2:0)+ - ((normals[i].z()>=0)?4:0); - } - stack.reserve(SIMPLE_STACKSIZE); - stack.push_back(sStkNP(root,0)); - do { - sStkNP se=stack[stack.size()-1]; - bool out=false; - stack.pop_back(); - for(int i=0,j=1;(!out)&&(ivolume.Classify(normals[i],offsets[i],signs[i]); - switch(side) - { - case -1: out=true;break; - case +1: se.mask|=j;break; - } - } - } - if(!out) - { - if((se.mask!=inside)&&(se.node->isinternal())) - { - stack.push_back(sStkNP(se.node->childs[0],se.mask)); - stack.push_back(sStkNP(se.node->childs[1],se.mask)); - } - else - { - if(policy.AllLeaves(se.node)) enumLeaves(se.node,policy); - } - } - } while(!stack.empty()); - }*/ - } - template - static void collideOCL( const NodeType* root, const AZ::Vector3* normals, const float* offsets, const AZ::Vector3& sortaxis, int count, Collector& collector, bool fullsort=true) - { - (void)root;(void)normals;(void)offsets;(void)sortaxis;(void)count;(void)offsets;(void)collector;(void)fullsort; -/* if(root) - { - const unsigned srtsgns=(sortaxis[0]>=0?1:0)+ - (sortaxis[1]>=0?2:0)+ - (sortaxis[2]>=0?4:0); - const int inside=(1< stock; - btAlignedObjectArray ifree; - btAlignedObjectArray stack; - int signs[sizeof(unsigned)*8]; - btAssert(count=0)?1:0)+ - ((normals[i].y()>=0)?2:0)+ - ((normals[i].z()>=0)?4:0); - } - stock.reserve(SIMPLE_STACKSIZE); - stack.reserve(SIMPLE_STACKSIZE); - ifree.reserve(SIMPLE_STACKSIZE); - stack.push_back(allocate(ifree,stock,sStkNPS(root,0,root->volume.ProjectMinimum(sortaxis,srtsgns)))); - do { - const int id=stack[stack.size()-1]; - sStkNPS se=stock[id]; - stack.pop_back();ifree.push_back(id); - if(se.mask!=inside) - { - bool out=false; - for(int i=0,j=1;(!out)&&(ivolume.Classify(normals[i],offsets[i],signs[i]); - switch(side) - { - case -1: out=true;break; - case +1: se.mask|=j;break; - } - } - } - if(out) continue; - } - if(policy.Descent(se.node)) - { - if(se.node->isinternal()) - { - const NodeType* pns[]={ se.node->childs[0],se.node->childs[1]}; - sStkNPS nes[]={ sStkNPS(pns[0],se.mask,pns[0]->volume.ProjectMinimum(sortaxis,srtsgns)), - sStkNPS(pns[1],se.mask,pns[1]->volume.ProjectMinimum(sortaxis,srtsgns))}; - const int q=nes[0].value0)) - { - // Insert 0 - j=nearest(&stack[0],&stock[0],nes[q].value,0,stack.size()); - stack.push_back(0); -#if DBVT_USE_MEMMOVE - memmove(&stack[j+1],&stack[j],sizeof(int)*(stack.size()-j-1)); -#else - for(int k=stack.size()-1;k>j;--k) stack[k]=stack[k-1]; -#endif - stack[j]=allocate(ifree,stock,nes[q]); - // Insert 1 - j=nearest(&stack[0],&stock[0],nes[1-q].value,j,stack.size()); - stack.push_back(0); -#if DBVT_USE_MEMMOVE - memmove(&stack[j+1],&stack[j],sizeof(int)*(stack.size()-j-1)); -#else - for(int k=stack.size()-1;k>j;--k) stack[k]=stack[k-1]; -#endif - stack[j]=allocate(ifree,stock,nes[1-q]); - } - else - { - stack.push_back(allocate(ifree,stock,nes[q])); - stack.push_back(allocate(ifree,stock,nes[1-q])); - } - } - else - { - policy.Process(se.node,se.value); - } - } - } while(stack.size()); - }*/ - } - - template - static void collideTU(const NodeType* root, Collector& collector) - { - (void)root;(void)collector; -/* if(root) - { - btAlignedObjectArray stack; - stack.reserve(SIMPLE_STACKSIZE); - stack.push_back(root); - do { - const NodeType* n=stack[stack.size()-1]; - stack.pop_back(); - if(policy.Descent(n)) - { - if(n->isinternal()) - { stack.push_back(n->childs[0]);stack.push_back(n->childs[1]); } - else - { policy.Process(n); } - } - } while(stack.size()>0); - }*/ - } - - private: - BvDynamicTree(const BvDynamicTree&) {} - - // Helpers - //static AZ_FORCE_INLINE int nearest(const int* i,const BvDynamicTree::sStkNPS* a,const float& v,int l,int h) - //{ - // int m=0; - // while(l>1; - // if(a[i[m]].value>=v) l=m+1; else h=m; - // } - // return h; - //} - //static AZ_FORCE_INLINE int allocate( int_fixed_stack_type& ifree, stknps_fixed_stack_type& stock, const sStkNPS& value) - //{ - // int i; - // if( !ifree.empty() ) - // { - // i=ifree[ifree.size()-1]; - // ifree.pop_back(); - // stock[i]=value; - // } - // else - // { - // i=stock.size(); - // stock.push_back(value); - // } - // return i; - //} - // - - AZ_FORCE_INLINE void deletenode( NodeType* node) - { - //btAlignedFree(pdbvt->m_free); - delete m_free; - m_free=node; - } - - void recursedeletenode( NodeType* node) - { - if(!node->IsLeaf()) - { - recursedeletenode(node->m_childs[0]); - recursedeletenode(node->m_childs[1]); - } - - if( node == m_root ) m_root=0; - deletenode(node); - } - - - AZ_FORCE_INLINE NodeType* createnode( NodeType* parent, void* data) - { - NodeType* node; - if(m_free) - { node=m_free;m_free=0; } - else - { node = aznew NodeType(); } - node->m_parent = parent; - node->m_data = data; - node->m_childs[1] = 0; - return node; - } - AZ_FORCE_INLINE NodeType* createnode( BvDynamicTree::NodeType* parent, const VolumeType& volume, void* data) - { - NodeType* node = createnode(parent,data); - node->m_volume=volume; - return node; - } - // - AZ_FORCE_INLINE NodeType* createnode( BvDynamicTree::NodeType* parent, const VolumeType& volume0, const VolumeType& volume1, void* data) - { - NodeType* node = createnode(parent,data); - Merge(volume0,volume1,node->m_volume); - return node; - } - void insertleaf( NodeType* root, NodeType* leaf); - NodeType* removeleaf( NodeType* leaf); - void fetchleaves(NodeType* root,NodeArrayType& leaves,int depth=-1); - void split(const NodeArrayType& leaves,NodeArrayType& left,NodeArrayType& right,const AZ::Vector3& org,const AZ::Vector3& axis); - VolumeType bounds(const NodeArrayType& leaves); - void bottomup( NodeArrayType& leaves ); - NodeType* topdown(NodeArrayType& leaves,int bu_treshold); - AZ_FORCE_INLINE NodeType* sort(NodeType* n,NodeType*& r); - - NodeType* m_root; - NodeType* m_free; - int m_lkhd; - int m_leaves; - unsigned m_opath; - - //btAlignedObjectArray m_stkStack; - // Profile and choose static or dynamic vector. - typedef AZStd::fixed_vector stknn_fixed_stack_type; - typedef AZStd::fixed_vector int_fixed_stack_type; - typedef AZStd::fixed_vector stknps_fixed_stack_type; - - stknn_fixed_stack_type m_stkStack; - }; -} - -#endif // RR_DYNAMIC_BOUNDING_VOLUME_TREE_H -#pragma once diff --git a/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.cpp b/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.cpp deleted file mode 100644 index 13a0151bc7..0000000000 --- a/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.cpp +++ /dev/null @@ -1,597 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#include - -#include -#include -#include - -#include -#include - -// for highly verbose internal debugging -//#define INTERNAL_DEBUG_PROXIMITY - -namespace GridMate -{ - - void ProximityInterestChunk::OnReplicaActivate(const ReplicaContext& rc) - { - m_interestHandler = static_cast(rc.m_rm->GetUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4))); - AZ_Warning("GridMate", m_interestHandler, "No proximity interest handler in the user context"); - - if (m_interestHandler) - { - m_interestHandler->OnNewRulesChunk(this, rc.m_peer); - } - } - - void ProximityInterestChunk::OnReplicaDeactivate(const ReplicaContext& rc) - { - if (rc.m_peer && m_interestHandler) - { - m_interestHandler->OnDeleteRulesChunk(this, rc.m_peer); - } - } - - bool ProximityInterestChunk::AddRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext& ctx) - { - if (IsProxy()) - { - auto rulePtr = m_interestHandler->CreateRule(ctx.m_sourcePeer); - rulePtr->Set(bbox); - m_rules.insert(AZStd::make_pair(netId, rulePtr)); - } - - return true; - } - - bool ProximityInterestChunk::RemoveRuleFn(RuleNetworkId netId, const RpcContext&) - { - if (IsProxy()) - { - m_rules.erase(netId); - } - - return true; - } - - bool ProximityInterestChunk::UpdateRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext&) - { - if (IsProxy()) - { - auto it = m_rules.find(netId); - if (it != m_rules.end()) - { - it->second->Set(bbox); - } - } - - return true; - } - - bool ProximityInterestChunk::AddRuleForPeerFn(RuleNetworkId netId, PeerId peerId, AZ::Aabb bbox, const RpcContext&) - { - ProximityInterestChunk* peerChunk = m_interestHandler->FindRulesChunkByPeerId(peerId); - if (peerChunk) - { - auto it = peerChunk->m_rules.find(netId); - if (it == peerChunk->m_rules.end()) - { - auto rulePtr = m_interestHandler->CreateRule(peerId); - peerChunk->m_rules.insert(AZStd::make_pair(netId, rulePtr)); - rulePtr->Set(bbox); - } - } - return false; - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterest - */ - ProximityInterest::ProximityInterest(ProximityInterestHandler* handler) - : m_handler(handler) - , m_bbox(AZ::Aabb::CreateNull()) - { - AZ_Assert(m_handler, "Invalid interest handler"); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestRule - */ - void ProximityInterestRule::Set(const AZ::Aabb& bbox) - { - m_bbox = bbox; - m_handler->UpdateRule(this); - } - - void ProximityInterestRule::Destroy() - { - m_handler->DestroyRule(this); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestAttribute - */ - void ProximityInterestAttribute::Set(const AZ::Aabb& bbox) - { - m_bbox = bbox; - m_handler->UpdateAttribute(this); - } - - void ProximityInterestAttribute::Destroy() - { - m_handler->DestroyAttribute(this); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestHandler - */ - ProximityInterestHandler::ProximityInterestHandler() - : m_im(nullptr) - , m_rm(nullptr) - , m_lastRuleNetId(0) - , m_rulesReplica(nullptr) - { - m_attributeWorld = AZStd::make_unique(); - AZ_Assert(m_attributeWorld, "Out of memory"); - } - - ProximityInterestRule::Ptr ProximityInterestHandler::CreateRule(PeerId peerId) - { - ProximityInterestRule* rulePtr = aznew ProximityInterestRule(this, peerId, GetNewRuleNetId()); - if (m_rm && peerId == m_rm->GetLocalPeerId()) - { - m_rulesReplica->AddRuleRpc(rulePtr->GetNetworkId(), rulePtr->Get()); - } - - CreateAndInsertIntoSpatialStructure(rulePtr); - - return rulePtr; - } - - ProximityInterestAttribute::Ptr ProximityInterestHandler::CreateAttribute(ReplicaId replicaId) - { - auto newAttribute = aznew ProximityInterestAttribute(this, replicaId); - AZ_Assert(newAttribute, "Out of memory"); - - CreateAndInsertIntoSpatialStructure(newAttribute); - - return newAttribute; - } - - void ProximityInterestHandler::FreeRule(ProximityInterestRule* rule) - { - //TODO: should be pool-allocated - delete rule; - } - - void ProximityInterestHandler::DestroyRule(ProximityInterestRule* rule) - { - if (m_rm && rule->GetPeerId() == m_rm->GetLocalPeerId()) - { - m_rulesReplica->RemoveRuleRpc(rule->GetNetworkId()); - } - - MarkAttributesDirtyInRule(rule); - - rule->m_bbox = AZ::Aabb::CreateNull(); - m_removedRules.insert(rule); - m_localRules.erase(rule); - } - - void ProximityInterestHandler::UpdateRule(ProximityInterestRule* rule) - { - if (m_rm && rule->GetPeerId() == m_rm->GetLocalPeerId()) - { - m_rulesReplica->UpdateRuleRpc(rule->GetNetworkId(), rule->Get()); - } - - m_dirtyRules.insert(rule); - } - - void ProximityInterestHandler::FreeAttribute(ProximityInterestAttribute* attrib) - { - delete attrib; - } - - void ProximityInterestHandler::DestroyAttribute(ProximityInterestAttribute* attrib) - { - RemoveFromSpatialStructure(attrib); - - m_attributes.erase(attrib); - m_removedAttributes.insert(attrib); - } - - void ProximityInterestHandler::RemoveFromSpatialStructure(ProximityInterestAttribute* attribute) - { - attribute->m_bbox = AZ::Aabb::CreateNull(); - m_attributeWorld->Remove(attribute->GetNode()); - attribute->SetNode(nullptr); - } - - void ProximityInterestHandler::UpdateAttribute(ProximityInterestAttribute* attrib) - { - auto node = attrib->GetNode(); - AZ_Assert(node, "Attribute wasn't created correctly"); - node->m_volume = attrib->Get(); - m_attributeWorld->Update(node); - - m_dirtyAttributes.insert(attrib); - } - - void ProximityInterestHandler::OnNewRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer) - { - if (chunk != m_rulesReplica) // non-local - { - m_peerChunks.insert(AZStd::make_pair(peer->GetId(), chunk)); - - for (auto& rule : m_localRules) - { - chunk->AddRuleForPeerRpc(rule->GetNetworkId(), rule->GetPeerId(), rule->Get()); - } - } - } - - void ProximityInterestHandler::OnDeleteRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer) - { - (void)chunk; - m_peerChunks.erase(peer->GetId()); - } - - RuleNetworkId ProximityInterestHandler::GetNewRuleNetId() - { - ++m_lastRuleNetId; - - if (m_rulesReplica) - { - return m_rulesReplica->GetReplicaId() | (static_cast(m_lastRuleNetId) << 32); - } - - return (static_cast(m_lastRuleNetId) << 32); - } - - ProximityInterestChunk* ProximityInterestHandler::FindRulesChunkByPeerId(PeerId peerId) - { - auto it = m_peerChunks.find(peerId); - if (it == m_peerChunks.end()) - { - return nullptr; - } - - return it->second; - } - - const InterestMatchResult& ProximityInterestHandler::GetLastResult() - { - return m_resultCache; - } - - ProximityInterestHandler::RuleSet& ProximityInterestHandler::GetAffectedRules() - { - /* - * The expectation that lots of attributes will change frequently, - * so there is no point in trying to optimize cases - * where only a few attributes have changed. - */ - if (m_dirtyAttributes.empty() && !m_dirtyRules.empty()) - { - return m_dirtyRules; - } - - /* - * Assuming all rules might have been affected. - * - * There is an optimization chance here if the number of rules is large, as in 1,000+ rules. - * To handle such scale we would need another spatial structure for rules. - */ - return m_localRules; - } - - void ProximityInterestHandler::GetAttributesWithinRule(ProximityInterestRule* rule, SpatialIndex::NodeCollector& nodes) - { - m_attributeWorld->Query(rule->Get(), nodes); - } - - void ProximityInterestHandler::ClearDirtyState() - { - m_dirtyAttributes.clear(); - m_dirtyRules.clear(); - } - - void ProximityInterestHandler::CreateAndInsertIntoSpatialStructure(ProximityInterestAttribute* attribute) - { - m_attributes.insert(attribute); - SpatialIndex::Node* node = m_attributeWorld->Insert(attribute->Get(), attribute); - attribute->SetNode(node); - } - - void ProximityInterestHandler::CreateAndInsertIntoSpatialStructure(ProximityInterestRule* rule) - { - m_localRules.insert(rule); - } - - void ProximityInterestHandler::UpdateInternal(InterestMatchResult& result) - { - /* - * The goal is to return all dirty attributes that were either dirty because: - * 1) they changed which rules have apply to - * 2) rules have changed and no longer apply to those attributes - * and thus resulted in different peer(s) associated with a given replica. - */ - - const RuleSet& rules = GetAffectedRules(); - - for (auto& dirtyAttribute : m_dirtyAttributes) - { - result.insert(dirtyAttribute->GetReplicaId()); - } - - /* - * The exectation is to have a lot more attributes than rules. - * The amount of rules should grow linear with amount of peers, - * so it should be OK to iterate through all rules each update. - */ - for (auto& rule : rules) - { - CheckChangesForRule(rule, result); - } - - for (auto& removedRule : m_removedRules) - { - FreeRule(removedRule); - } - m_removedRules.clear(); - - // mark removed attribute as having no peers - for (auto& removedAttribute : m_removedAttributes) - { - result.insert(removedAttribute->GetReplicaId()); - FreeAttribute(removedAttribute); - } - m_removedAttributes.clear(); - } - - void ProximityInterestHandler::CheckChangesForRule(ProximityInterestRule* rule, InterestMatchResult& result) - { - SpatialIndex::NodeCollector collector; - GetAttributesWithinRule(rule, collector); - - auto peerId = rule->GetPeerId(); - for (ProximityInterestAttribute* attr : collector.GetNodes()) - { - AZ_Assert(attr, "bad node?"); - - auto findIt = result.find(attr->GetReplicaId()); - if (findIt != result.end()) - { - findIt->second.insert(peerId); - } - else - { - auto resultIt = result.insert(attr->GetReplicaId()); - AZ_Assert(resultIt.second, "Successfully inserted"); - resultIt.first->second.insert(peerId); - } - } - } - - void ProximityInterestHandler::MarkAttributesDirtyInRule(ProximityInterestRule* rule) - { - SpatialIndex::NodeCollector collector; - GetAttributesWithinRule(rule, collector); - - for (ProximityInterestAttribute* attr : collector.GetNodes()) - { - AZ_Assert(attr, "bad node?"); - - UpdateAttribute(attr); - } - } - - void ProximityInterestHandler::ProduceChanges(const InterestMatchResult& before, const InterestMatchResult& after) - { - m_resultCache.clear(); - -#if defined(INTERNAL_DEBUG_PROXIMITY) - before.PrintMatchResult("before"); - after.PrintMatchResult("after"); -#endif - - /* - * 'after' contains only the stuff that might have changed - */ - for (auto& possiblyDirty : after) - { - ReplicaId repId = possiblyDirty.first; - const InterestPeerSet& peerSet = possiblyDirty.second; - - auto foundInBefore = before.find(repId); - if (foundInBefore != before.end()) - { - if (!HasSamePeers(foundInBefore->second, peerSet)) - { - // was in the last calculation but has a different peer set now - m_resultCache.insert(AZStd::make_pair(repId, peerSet)); - } - } - else - { - // since it wasn't present during last calculation - m_resultCache.insert(AZStd::make_pair(repId, peerSet)); - } - } - - // Mark attributes (replicas) for removal that have not moved but a rule (clients) no longer sees it - for (auto& possiblyDirty : before) - { - ReplicaId repId = possiblyDirty.first; - - const auto foundInAfter = after.find(repId); - /* - * If the prior state was a replica A present on peer X: "A{X}", and now A should no longer be present on any peer: "A{}" - * then by the rules of InterestHandlers interacting with InterestManager, we should return in @m_resultCache the following: - * - * A{} - indicating that replica A must be removed all peers. - * - * On the next pass, the prior state would be: "A{}" and the current state would be "A{}" as well. At that point, we have - * already sent the update to remove A from X, so @m_resultCache should no longer mention A at all. - */ - if (foundInAfter == after.end() && !possiblyDirty.second.empty() /* "not A{}" see the above comment */) - { - m_resultCache.insert(AZStd::make_pair(repId, InterestPeerSet())); - } - } - -#if defined(INTERNAL_DEBUG_PROXIMITY) - m_resultCache.PrintMatchResult("changes"); -#endif - - } - - bool ProximityInterestHandler::HasSamePeers(const InterestPeerSet& one, const InterestPeerSet& another) - { - if (one.size() != another.size()) - { - return false; - } - - for (auto& peerFromOne : one) - { - if (another.find(peerFromOne) == another.end()) - { - return false; - } - } - - // Safe to assume it's the same sets since all entries are unique in a peer sets - return true; - } - - void ProximityInterestHandler::Update() - { - InterestMatchResult newResult; - - UpdateInternal(newResult); - ProduceChanges(m_lastResult, newResult); - - m_lastResult = std::move(newResult); - ClearDirtyState(); - } - - void ProximityInterestHandler::OnRulesHandlerRegistered(InterestManager* manager) - { - AZ_Assert(m_im == nullptr, "Handler is already registered with manager %p (%p)\n", m_im, manager); - AZ_Assert(m_rulesReplica == nullptr, "Rules replica is already created\n"); - AZ_TracePrintf("GridMate", "Proximity interest handler is registered\n"); - m_im = manager; - m_rm = m_im->GetReplicaManager(); - m_rm->RegisterUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4), this); - - auto replica = Replica::CreateReplica("ProximityInterestHandlerRules"); - m_rulesReplica = CreateAndAttachReplicaChunk(replica); - m_rm->AddPrimary(replica); - } - - void ProximityInterestHandler::OnRulesHandlerUnregistered(InterestManager* manager) - { - (void)manager; - AZ_Assert(m_im == manager, "Handler was not registered with manager %p (%p)\n", manager, m_im); - AZ_TracePrintf("GridMate", "Proximity interest handler is unregistered\n"); - m_rulesReplica = nullptr; - m_im = nullptr; - m_rm->UnregisterUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4)); - m_rm = nullptr; - - for (auto& chunk : m_peerChunks) - { - chunk.second->m_interestHandler = nullptr; - } - m_peerChunks.clear(); - - ClearDirtyState(); - DestroyAll(); - - m_resultCache.clear(); - } - - void ProximityInterestHandler::DestroyAll() - { - for (ProximityInterestRule* rule : m_localRules) - { - FreeRule(rule); - } - m_localRules.clear(); - - for (ProximityInterestAttribute* attr : m_attributes) - { - FreeAttribute(attr); - } - m_attributes.clear(); - - for (auto& removedRule : m_removedRules) - { - FreeRule(removedRule); - } - m_removedRules.clear(); - - for (auto& removedAttribute : m_removedAttributes) - { - FreeAttribute(removedAttribute); - } - m_removedAttributes.clear(); - } - - /////////////////////////////////////////////////////////////////////////// - ProximityInterestHandler::~ProximityInterestHandler() - { - /* - * If a handler was registered with a InterestManager, then InterestManager ought to have called OnRulesHandlerUnregistered - * but this is a safety pre-caution. - */ - DestroyAll(); - } - - SpatialIndex::SpatialIndex() - { - m_tree.reset(aznew GridMate::BvDynamicTree()); - } - - void SpatialIndex::Remove(Node* node) - { - m_tree->Remove(node); - } - - void SpatialIndex::Update(Node* node) - { - m_tree->Update(node); - } - - SpatialIndex::Node* SpatialIndex::Insert(const AZ::Aabb& get, ProximityInterestAttribute* attribute) - { - return m_tree->Insert(get, attribute); - } - - void SpatialIndex::Query(const AZ::Aabb& shape, NodeCollector& nodes) - { - m_tree->collideTV(m_tree->GetRoot(), shape, nodes); - } -} diff --git a/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.h b/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.h deleted file mode 100644 index 6659d44f44..0000000000 --- a/Code/Framework/GridMate/GridMate/Replica/Interest/ProximityInterestHandler.h +++ /dev/null @@ -1,314 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#ifndef GM_REPLICA_PROXIMITYINTERESTHANDLER_H -#define GM_REPLICA_PROXIMITYINTERESTHANDLER_H - -#include -#include -#include -#include -#include - -#include -#include - -namespace GridMate -{ - class ProximityInterestHandler; - class ProximityInterestAttribute; - - /* - * Base interest - */ - class ProximityInterest - { - friend class ProximityInterestHandler; - - public: - const AZ::Aabb& Get() const { return m_bbox; } - - protected: - explicit ProximityInterest(ProximityInterestHandler* handler); - - ProximityInterestHandler* m_handler; - AZ::Aabb m_bbox; - }; - /////////////////////////////////////////////////////////////////////////// - - - /* - * Proximity rule - */ - class ProximityInterestRule - : public InterestRule - , public ProximityInterest - { - friend class ProximityInterestHandler; - - public: - using Ptr = AZStd::intrusive_ptr; - - GM_CLASS_ALLOCATOR(ProximityInterestRule); - - void Set(const AZ::Aabb& bbox); - - private: - - // Intrusive ptr - template - friend struct AZStd::IntrusivePtrCountPolicy; - unsigned int m_refCount = 0; - AZ_FORCE_INLINE void add_ref() { ++m_refCount; } - AZ_FORCE_INLINE void release() { --m_refCount; if (!m_refCount) Destroy(); } - AZ_FORCE_INLINE bool IsDeleted() const { return m_refCount == 0; } - /////////////////////////////////////////////////////////////////////////// - - ProximityInterestRule(ProximityInterestHandler* handler, PeerId peerId, RuleNetworkId netId) - : InterestRule(peerId, netId) - , ProximityInterest(handler) - {} - - void Destroy(); - }; - /////////////////////////////////////////////////////////////////////////// - - class SpatialIndex - { - public: - typedef Internal::DynamicTreeNode Node; - - class NodeCollector - { - typedef AZStd::vector Type; - - public: - void Process(const Internal::DynamicTreeNode* node) - { - m_nodes.push_back(reinterpret_cast(node->m_data)); - } - - const Type& GetNodes() const - { - return m_nodes; - } - - private: - Type m_nodes; - }; - - SpatialIndex(); - ~SpatialIndex() = default; - - AZ_FORCE_INLINE void Remove(Node* node); - AZ_FORCE_INLINE void Update(Node* node); - AZ_FORCE_INLINE Node* Insert(const AZ::Aabb& get, ProximityInterestAttribute* attribute); - AZ_FORCE_INLINE void Query(const AZ::Aabb& get, NodeCollector& nodes); - - private: - AZStd::unique_ptr m_tree; - }; - - /* - * Proximity attribute - */ - class ProximityInterestAttribute - : public InterestAttribute - , public ProximityInterest - { - friend class ProximityInterestHandler; - template friend class InterestPtr; - - public: - using Ptr = AZStd::intrusive_ptr; - - GM_CLASS_ALLOCATOR(ProximityInterestAttribute); - - void Set(const AZ::Aabb& bbox); - - private: - - // Intrusive ptr - template - friend struct AZStd::IntrusivePtrCountPolicy; - unsigned int m_refCount = 0; - AZ_FORCE_INLINE void add_ref() { ++m_refCount; } - AZ_FORCE_INLINE void release() { Destroy(); } - AZ_FORCE_INLINE bool IsDeleted() const { return m_refCount == 0; } - /////////////////////////////////////////////////////////////////////////// - - ProximityInterestAttribute(ProximityInterestHandler* handler, ReplicaId repId) - : InterestAttribute(repId) - , ProximityInterest(handler) - , m_worldNode(nullptr) - {} - - void Destroy(); - - void SetNode(SpatialIndex::Node* node) { m_worldNode = node; } - SpatialIndex::Node* GetNode() const { return m_worldNode; } - SpatialIndex::Node* m_worldNode; ///< non-owning pointer - }; - /////////////////////////////////////////////////////////////////////////// - - class ProximityInterestChunk - : public ReplicaChunk - { - public: - GM_CLASS_ALLOCATOR(ProximityInterestChunk); - - // ReplicaChunk - typedef AZStd::intrusive_ptr Ptr; - bool IsReplicaMigratable() override { return false; } - bool IsBroadcast() override { return true; } - static const char* GetChunkName() { return "ProximityInterestChunk"; } - - ProximityInterestChunk() - : AddRuleRpc("AddRule") - , RemoveRuleRpc("RemoveRule") - , UpdateRuleRpc("UpdateRule") - , AddRuleForPeerRpc("AddRuleForPeerRpc") - , m_interestHandler(nullptr) - { - } - - void OnReplicaActivate(const ReplicaContext& rc) override; - void OnReplicaDeactivate(const ReplicaContext& rc) override; - - bool AddRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext& ctx); - bool RemoveRuleFn(RuleNetworkId netId, const RpcContext&); - bool UpdateRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext&); - bool AddRuleForPeerFn(RuleNetworkId netId, PeerId peerId, AZ::Aabb bbox, const RpcContext&); - - Rpc, RpcArg>::BindInterface AddRuleRpc; - Rpc>::BindInterface RemoveRuleRpc; - Rpc, RpcArg>::BindInterface UpdateRuleRpc; - - Rpc, RpcArg, RpcArg>::BindInterface AddRuleForPeerRpc; - - unordered_map m_rules; - ProximityInterestHandler* m_interestHandler; - }; - - /* - * Rules handler - */ - class ProximityInterestHandler - : public BaseRulesHandler - { - friend class ProximityInterestRule; - friend class ProximityInterestAttribute; - friend class ProximityInterestChunk; - - public: - - typedef unordered_set AttributeSet; - typedef unordered_set RuleSet; - - GM_CLASS_ALLOCATOR(ProximityInterestHandler); - - ProximityInterestHandler(); - ~ProximityInterestHandler(); - - /* - * Creates new proximity rule and binds it to the peer. - * Note: the lifetime of the created rule is tied to the lifetime of this handler. - */ - ProximityInterestRule::Ptr CreateRule(PeerId peerId); - - /* - * Creates new proximity attribute and binds it to the replica. - * Note: the lifetime of the created attribute is tied to the lifetime of this handler. - */ - ProximityInterestAttribute::Ptr CreateAttribute(ReplicaId replicaId); - - // Calculates rules and attributes matches - void Update() override; - - // Returns last recalculated results - const InterestMatchResult& GetLastResult() override; - - // Returns the manager it's bound to - InterestManager* GetManager() override { return m_im; } - - // Rules that this handler is aware of - const RuleSet& GetLocalRules() const { return m_localRules; } - - private: - - // BaseRulesHandler - void OnRulesHandlerRegistered(InterestManager* manager) override; - void OnRulesHandlerUnregistered(InterestManager* manager) override; - - void DestroyRule(ProximityInterestRule* rule); - void FreeRule(ProximityInterestRule* rule); - void UpdateRule(ProximityInterestRule* rule); - - void DestroyAttribute(ProximityInterestAttribute* attrib); - void FreeAttribute(ProximityInterestAttribute* attrib); - void UpdateAttribute(ProximityInterestAttribute* attrib); - - void OnNewRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer); - void OnDeleteRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer); - - RuleNetworkId GetNewRuleNetId(); - - ProximityInterestChunk* FindRulesChunkByPeerId(PeerId peerId); - - void DestroyAll(); - - InterestManager* m_im; - ReplicaManager* m_rm; - - AZ::u32 m_lastRuleNetId; - - unordered_map m_peerChunks; - - RuleSet m_localRules; - RuleSet m_removedRules; - RuleSet m_dirtyRules; - - AttributeSet m_attributes; - AttributeSet m_removedAttributes; - AttributeSet m_dirtyAttributes; - - ProximityInterestChunk* m_rulesReplica; - - // collection of all known attributes - AZStd::unique_ptr m_attributeWorld; - - InterestMatchResult m_resultCache; - - /////////////////////////////////////////////////////////////////////////////////////////////////// - // internal processing helpers - AZ_FORCE_INLINE RuleSet& GetAffectedRules(); - AZ_FORCE_INLINE void GetAttributesWithinRule(ProximityInterestRule* rule, SpatialIndex::NodeCollector& nodes); - AZ_FORCE_INLINE void ClearDirtyState(); - - AZ_FORCE_INLINE void CreateAndInsertIntoSpatialStructure(ProximityInterestAttribute* attribute); - AZ_FORCE_INLINE void RemoveFromSpatialStructure(ProximityInterestAttribute* attribute); - AZ_FORCE_INLINE void CreateAndInsertIntoSpatialStructure(ProximityInterestRule* rule); - - void UpdateInternal(InterestMatchResult& result); - void CheckChangesForRule(ProximityInterestRule* rule, InterestMatchResult& result); - void MarkAttributesDirtyInRule(ProximityInterestRule* rule); - - static bool HasSamePeers(const InterestPeerSet& one, const InterestPeerSet& another); - void ProduceChanges(const InterestMatchResult& before, const InterestMatchResult& after); - - InterestMatchResult m_lastResult; - /////////////////////////////////////////////////////////////////////////////////////////////////// - }; - /////////////////////////////////////////////////////////////////////////// -} - -#endif // GM_REPLICA_PROXIMITYINTERESTHANDLER_H diff --git a/Code/Framework/GridMate/GridMate/gridmate_files.cmake b/Code/Framework/GridMate/GridMate/gridmate_files.cmake index 7cc65ff69a..615ce53027 100644 --- a/Code/Framework/GridMate/GridMate/gridmate_files.cmake +++ b/Code/Framework/GridMate/GridMate/gridmate_files.cmake @@ -100,14 +100,10 @@ set(FILES Replica/Tasks/ReplicaPriorityPolicy.h Replica/Interest/BitmaskInterestHandler.cpp Replica/Interest/BitmaskInterestHandler.h - Replica/Interest/ProximityInterestHandler.cpp - Replica/Interest/ProximityInterestHandler.h Replica/Interest/InterestDefs.h Replica/Interest/InterestManager.cpp Replica/Interest/InterestManager.h Replica/Interest/InterestQueryResult.h - Replica/Interest/BvDynamicTree.cpp - Replica/Interest/BvDynamicTree.h Replica/Interest/RulesHandler.h Serialize/Buffer.cpp Serialize/Buffer.h diff --git a/Code/Framework/GridMate/Tests/Interest.cpp b/Code/Framework/GridMate/Tests/Interest.cpp deleted file mode 100644 index c449c92083..0000000000 --- a/Code/Framework/GridMate/Tests/Interest.cpp +++ /dev/null @@ -1,1823 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ -#include "Tests.h" -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace GridMate; - -// An easy switch to use an old brute force ProximityInterestHandler for performance comparison. -#if 0 -namespace GridMate -{ - /* - * Base interest - */ - class ProximityInterest - { - friend class ProximityInterestHandler; - - public: - const AZ::Aabb& Get() const { return m_bbox; } - - protected: - explicit ProximityInterest(ProximityInterestHandler* handler); - - ProximityInterestHandler* m_handler; - AZ::Aabb m_bbox; - }; - /////////////////////////////////////////////////////////////////////////// - - - /* - * Proximity rule - */ - class ProximityInterestRule - : public InterestRule - , public ProximityInterest - { - friend class ProximityInterestHandler; - - public: - using Ptr = AZStd::intrusive_ptr; - - GM_CLASS_ALLOCATOR(ProximityInterestRule); - - void Set(const AZ::Aabb& bbox); - - private: - - // Intrusive ptr - template - friend struct AZStd::IntrusivePtrCountPolicy; - unsigned int m_refCount = 0; - AZ_FORCE_INLINE void add_ref() { ++m_refCount; } - AZ_FORCE_INLINE void release() { --m_refCount; if (!m_refCount) Destroy(); } - AZ_FORCE_INLINE bool IsDeleted() const { return m_refCount == 0; } - /////////////////////////////////////////////////////////////////////////// - - ProximityInterestRule(ProximityInterestHandler* handler, PeerId peerId, RuleNetworkId netId) - : InterestRule(peerId, netId) - , ProximityInterest(handler) - {} - - void Destroy(); - }; - /////////////////////////////////////////////////////////////////////////// - - - /* - * Proximity attribute - */ - class ProximityInterestAttribute - : public InterestAttribute - , public ProximityInterest - { - friend class ProximityInterestHandler; - template friend class InterestPtr; - - public: - using Ptr = AZStd::intrusive_ptr; - - GM_CLASS_ALLOCATOR(ProximityInterestAttribute); - - void Set(const AZ::Aabb& bbox); - - private: - - // Intrusive ptr - template - friend struct AZStd::IntrusivePtrCountPolicy; - unsigned int m_refCount = 0; - AZ_FORCE_INLINE void add_ref() { ++m_refCount; } - AZ_FORCE_INLINE void release() { Destroy(); } - AZ_FORCE_INLINE bool IsDeleted() const { return m_refCount == 0; } - /////////////////////////////////////////////////////////////////////////// - - ProximityInterestAttribute(ProximityInterestHandler* handler, ReplicaId repId) - : InterestAttribute(repId) - , ProximityInterest(handler) - {} - - void Destroy(); - }; - /////////////////////////////////////////////////////////////////////////// - - - /* - * Rules handler - */ - class ProximityInterestHandler - : public BaseRulesHandler - { - friend class ProximityInterestRule; - friend class ProximityInterestAttribute; - friend class ProximityInterestChunk; - - public: - - typedef unordered_set AttributeSet; - typedef unordered_set RuleSet; - - GM_CLASS_ALLOCATOR(ProximityInterestHandler); - - ProximityInterestHandler(); - - // Creates new proximity rule and binds it to the peer - ProximityInterestRule::Ptr CreateRule(PeerId peerId); - - // Creates new proximity attribute and binds it to the replica - ProximityInterestAttribute::Ptr CreateAttribute(ReplicaId replicaId); - - // Calculates rules and attributes matches - void Update() override; - - // Returns last recalculated results - const InterestMatchResult& GetLastResult() override; - - // Returns manager its bound with - InterestManager* GetManager() override { return m_im; } - - const RuleSet& GetLocalRules() { return m_localRules; } - - private: - void UpdateInternal(InterestMatchResult& result); - - // BaseRulesHandler - void OnRulesHandlerRegistered(InterestManager* manager) override; - void OnRulesHandlerUnregistered(InterestManager* manager) override; - - void DestroyRule(ProximityInterestRule* rule); - void FreeRule(ProximityInterestRule* rule); - void UpdateRule(ProximityInterestRule* rule); - - void DestroyAttribute(ProximityInterestAttribute* attrib); - void FreeAttribute(ProximityInterestAttribute* attrib); - void UpdateAttribute(ProximityInterestAttribute* attrib); - - - void OnNewRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer); - void OnDeleteRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer); - - RuleNetworkId GetNewRuleNetId(); - - ProximityInterestChunk* FindRulesChunkByPeerId(PeerId peerId); - - InterestManager* m_im; - ReplicaManager* m_rm; - - AZ::u32 m_lastRuleNetId; - - unordered_map m_peerChunks; - RuleSet m_localRules; - - AttributeSet m_attributes; - RuleSet m_rules; - - ProximityInterestChunk* m_rulesReplica; - - InterestMatchResult m_resultCache; - }; - /////////////////////////////////////////////////////////////////////////// - - class ProximityInterestChunk - : public ReplicaChunk - { - public: - GM_CLASS_ALLOCATOR(ProximityInterestChunk); - - // ReplicaChunk - typedef AZStd::intrusive_ptr Ptr; - bool IsReplicaMigratable() override { return false; } - static const char* GetChunkName() { return "ProximityInterestChunk"; } - bool IsBroadcast() { return true; } - /////////////////////////////////////////////////////////////////////////// - - - ProximityInterestChunk() - : m_interestHandler(nullptr) - , AddRuleRpc("AddRule") - , RemoveRuleRpc("RemoveRule") - , UpdateRuleRpc("UpdateRule") - , AddRuleForPeerRpc("AddRuleForPeerRpc") - { - - } - - void OnReplicaActivate(const ReplicaContext& rc) override - { - m_interestHandler = static_cast(rc.m_rm->GetUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4))); - AZ_Assert(m_interestHandler, "No proximity interest handler in the user context"); - - if (m_interestHandler) - { - m_interestHandler->OnNewRulesChunk(this, rc.m_peer); - } - } - - void OnReplicaDeactivate(const ReplicaContext& rc) override - { - if (rc.m_peer && m_interestHandler) - { - m_interestHandler->OnDeleteRulesChunk(this, rc.m_peer); - } - } - - bool AddRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext& ctx) - { - if (IsProxy()) - { - auto rulePtr = m_interestHandler->CreateRule(ctx.m_sourcePeer); - rulePtr->Set(bbox); - m_rules.insert(AZStd::make_pair(netId, rulePtr)); - } - - return true; - } - - bool RemoveRuleFn(RuleNetworkId netId, const RpcContext&) - { - if (IsProxy()) - { - m_rules.erase(netId); - } - - return true; - } - - bool UpdateRuleFn(RuleNetworkId netId, AZ::Aabb bbox, const RpcContext&) - { - if (IsProxy()) - { - auto it = m_rules.find(netId); - if (it != m_rules.end()) - { - it->second->Set(bbox); - } - } - - return true; - } - - bool AddRuleForPeerFn(RuleNetworkId netId, PeerId peerId, AZ::Aabb bbox, const RpcContext&) - { - ProximityInterestChunk* peerChunk = m_interestHandler->FindRulesChunkByPeerId(peerId); - if (peerChunk) - { - auto it = peerChunk->m_rules.find(netId); - if (it == peerChunk->m_rules.end()) - { - auto rulePtr = m_interestHandler->CreateRule(peerId); - peerChunk->m_rules.insert(AZStd::make_pair(netId, rulePtr)); - rulePtr->Set(bbox); - } - } - return false; - } - - Rpc, RpcArg>::BindInterface AddRuleRpc; - Rpc>::BindInterface RemoveRuleRpc; - Rpc, RpcArg>::BindInterface UpdateRuleRpc; - - Rpc, RpcArg, RpcArg>::BindInterface AddRuleForPeerRpc; - - ProximityInterestHandler* m_interestHandler; - unordered_map m_rules; - }; - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterest - */ - ProximityInterest::ProximityInterest(ProximityInterestHandler* handler) - : m_bbox(AZ::Aabb::CreateNull()) - , m_handler(handler) - { - AZ_Assert(m_handler, "Invalid interest handler"); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestRule - */ - void ProximityInterestRule::Set(const AZ::Aabb& bbox) - { - m_bbox = bbox; - m_handler->UpdateRule(this); - } - - void ProximityInterestRule::Destroy() - { - m_handler->DestroyRule(this); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestAttribute - */ - void ProximityInterestAttribute::Set(const AZ::Aabb& bbox) - { - m_bbox = bbox; - m_handler->UpdateAttribute(this); - } - - void ProximityInterestAttribute::Destroy() - { - m_handler->DestroyAttribute(this); - } - /////////////////////////////////////////////////////////////////////////// - - - /* - * ProximityInterestHandler - */ - ProximityInterestHandler::ProximityInterestHandler() - : m_im(nullptr) - , m_rm(nullptr) - , m_lastRuleNetId(0) - , m_rulesReplica(nullptr) - { - if (!ReplicaChunkDescriptorTable::Get().FindReplicaChunkDescriptor(ReplicaChunkClassId(ProximityInterestChunk::GetChunkName()))) - { - ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - } - } - - ProximityInterestRule::Ptr ProximityInterestHandler::CreateRule(PeerId peerId) - { - ProximityInterestRule* rulePtr = aznew ProximityInterestRule(this, peerId, GetNewRuleNetId()); - if (peerId == m_rm->GetLocalPeerId()) - { - m_rulesReplica->AddRuleRpc(rulePtr->GetNetworkId(), rulePtr->Get()); - m_localRules.insert(rulePtr); - } - - return rulePtr; - } - - void ProximityInterestHandler::FreeRule(ProximityInterestRule* rule) - { - //TODO: should be pool-allocated - delete rule; - } - - void ProximityInterestHandler::DestroyRule(ProximityInterestRule* rule) - { - if (m_rm && rule->GetPeerId() == m_rm->GetLocalPeerId()) - { - m_rulesReplica->RemoveRuleRpc(rule->GetNetworkId()); - } - - rule->m_bbox = AZ::Aabb::CreateNull(); - m_rules.insert(rule); - m_localRules.erase(rule); - } - - void ProximityInterestHandler::UpdateRule(ProximityInterestRule* rule) - { - if (rule->GetPeerId() == m_rm->GetLocalPeerId()) - { - m_rulesReplica->UpdateRuleRpc(rule->GetNetworkId(), rule->Get()); - } - - m_rules.insert(rule); - } - - ProximityInterestAttribute::Ptr ProximityInterestHandler::CreateAttribute(ReplicaId replicaId) - { - return aznew ProximityInterestAttribute(this, replicaId); - } - - void ProximityInterestHandler::FreeAttribute(ProximityInterestAttribute* attrib) - { - //TODO: should be pool-allocated - delete attrib; - } - - void ProximityInterestHandler::DestroyAttribute(ProximityInterestAttribute* attrib) - { - attrib->m_bbox = AZ::Aabb::CreateNull(); - m_attributes.insert(attrib); - } - - void ProximityInterestHandler::UpdateAttribute(ProximityInterestAttribute* attrib) - { - m_attributes.insert(attrib); - } - - void ProximityInterestHandler::OnNewRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer) - { - if (chunk != m_rulesReplica) // non-local - { - m_peerChunks.insert(AZStd::make_pair(peer->GetId(), chunk)); - - for (auto& rule : m_localRules) - { - chunk->AddRuleForPeerRpc(rule->GetNetworkId(), rule->GetPeerId(), rule->Get()); - } - } - } - - void ProximityInterestHandler::OnDeleteRulesChunk(ProximityInterestChunk* chunk, ReplicaPeer* peer) - { - (void)chunk; - m_peerChunks.erase(peer->GetId()); - } - - RuleNetworkId ProximityInterestHandler::GetNewRuleNetId() - { - ++m_lastRuleNetId; - return m_rulesReplica->GetReplicaId() | (static_cast(m_lastRuleNetId) << 32); - } - - ProximityInterestChunk* ProximityInterestHandler::FindRulesChunkByPeerId(PeerId peerId) - { - auto it = m_peerChunks.find(peerId); - if (it == m_peerChunks.end()) - { - return nullptr; - } - else - { - return it->second; - } - } - - const InterestMatchResult& ProximityInterestHandler::GetLastResult() - { - return m_resultCache; - } - - void ProximityInterestHandler::UpdateInternal(InterestMatchResult& result) - { - ////////////////////////////////////////////// - // just recalculating the whole state for now - for (auto attrIt = m_attributes.begin(); attrIt != m_attributes.end(); ) - { - ProximityInterestAttribute* attr = *attrIt; - - auto resultIt = result.insert(attr->GetReplicaId()); - for (auto ruleIt = m_rules.begin(); ruleIt != m_rules.end(); ++ruleIt) - { - ProximityInterestRule* rule = *ruleIt; - if (rule->m_bbox.Overlaps(attr->m_bbox)) - { - resultIt.first->second.insert(rule->GetPeerId()); - } - } - - if ((*attrIt)->IsDeleted()) - { - attrIt = m_attributes.erase(attrIt); - delete attr; - } - else - { - ++attrIt; - } - } - - for (auto ruleIt = m_rules.begin(); ruleIt != m_rules.end(); ) - { - ProximityInterestRule* rule = *ruleIt; - - if (rule->IsDeleted()) - { - ruleIt = m_rules.erase(ruleIt); - delete rule; - } - else - { - ++ruleIt; - } - } - ////////////////////////////////////////////// - } - - void ProximityInterestHandler::Update() - { - m_resultCache.clear(); - - UpdateInternal(m_resultCache); - } - - void ProximityInterestHandler::OnRulesHandlerRegistered(InterestManager* manager) - { - AZ_Assert(m_im == nullptr, "Handler is already registered with manager %p (%p)\n", m_im, manager); - AZ_Assert(m_rulesReplica == nullptr, "Rules replica is already created\n"); - AZ_TracePrintf("GridMate", "Proximity interest handler is registered\n"); - m_im = manager; - m_rm = m_im->GetReplicaManager(); - m_rm->RegisterUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4), this); - - auto replica = Replica::CreateReplica("ProximityInterestHandlerRules"); - m_rulesReplica = CreateAndAttachReplicaChunk(replica); - m_rm->AddPrimary(replica); - } - - void ProximityInterestHandler::OnRulesHandlerUnregistered(InterestManager* manager) - { - (void)manager; - AZ_Assert(m_im == manager, "Handler was not registered with manager %p (%p)\n", manager, m_im); - AZ_TracePrintf("GridMate", "Proximity interest handler is unregistered\n"); - m_rulesReplica = nullptr; - m_im = nullptr; - m_rm->UnregisterUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4)); - m_rm = nullptr; - - for (auto& chunk : m_peerChunks) - { - chunk.second->m_interestHandler = nullptr; - } - m_peerChunks.clear(); - m_localRules.clear(); - - for (ProximityInterestRule* rule : m_rules) - { - delete rule; - } - - for (ProximityInterestAttribute* attr : m_attributes) - { - delete attr; - } - - m_attributes.clear(); - m_rules.clear(); - - m_resultCache.clear(); - } - /////////////////////////////////////////////////////////////////////////// -} -#else -// Optimized spatial handler. -#include "GridMate/Replica/Interest/ProximityInterestHandler.h" -#endif - -namespace UnitTest { - -/* - * Helper class to capture performance of various Interest Managers - */ -class PerfForInterestManager -{ -public: - void Reset(); - - void PreUpdate(); - void PostUpdate(); - - AZ::u32 GetTotalFrames() const; - float GetAverageFrame() const; - float GetWorstFrame() const; - float GetBestFrame() const; - -private: - AZ::Debug::Timer m_timer; - AZ::u32 m_frameCount = 0; - float m_totalUpdateTime = 0.f; - float m_fastestFrame = 100.f; - float m_slowestFrame = 0.f; -}; - -PerfForInterestManager g_PerfIM = PerfForInterestManager(); -PerfForInterestManager g_PerfUpdatingAttributes = PerfForInterestManager(); - -/* -* Utility function to tick the replica manager -*/ -static void UpdateReplicas(ReplicaManager* replicaManager, InterestManager* interestManager) -{ - if (interestManager) - { - // Measuring time it takes to execute an update. - g_PerfIM.PreUpdate(); - interestManager->Update(); - g_PerfIM.PostUpdate(); - } - - if (replicaManager) - { - replicaManager->Unmarshal(); - replicaManager->UpdateFromReplicas(); - replicaManager->UpdateReplicas(); - replicaManager->Marshal(); - } -} - -class Integ_InterestTest - : public GridMateMPTestFixture -{ - /////////////////////////////////////////////////////////////////// - class InterestTestChunk - : public ReplicaChunk - { - public: - GM_CLASS_ALLOCATOR(InterestTestChunk); - - InterestTestChunk() - : m_data("Data", 0) - , m_bitmaskAttributeData("BitmaskAttributeData") - , m_attribute(nullptr) - { - } - - /////////////////////////////////////////////////////////////////// - typedef AZStd::intrusive_ptr Ptr; - static const char* GetChunkName() { return "InterestTestChunk"; } - bool IsReplicaMigratable() override { return false; } - bool IsBroadcast() override - { - return false; - } - /////////////////////////////////////////////////////////////////// - - void OnReplicaActivate(const ReplicaContext& rc) override - { - AZ_Printf("GridMate", "InterestTestChunk::OnReplicaActivate repId=%08X(%s) fromPeerId=%08X localPeerId=%08X\n", - GetReplicaId(), - IsPrimary() ? "primary" : "proxy", - rc.m_peer ? rc.m_peer->GetId() : 0, - rc.m_rm->GetLocalPeerId()); - - BitmaskInterestHandler* ih = static_cast(rc.m_rm->GetUserContext(AZ_CRC("BitmaskInterestHandler", 0x5bf5d75b))); - if (ih) - { - m_attribute = ih->CreateAttribute(GetReplicaId()); - m_attribute->Set(m_bitmaskAttributeData.Get()); - } - } - - void OnReplicaDeactivate(const ReplicaContext& rc) override - { - AZ_Printf("GridMate", "InterestTestChunk::OnReplicaDeactivate repId=%08X(%s) fromPeerId=%08X localPeerId=%08X\n", - GetReplicaId(), - IsPrimary() ? "primary" : "proxy", - rc.m_peer ? rc.m_peer->GetId() : 0, - rc.m_rm->GetLocalPeerId()); - - m_attribute = nullptr; - } - - void BitmaskHandler(const InterestBitmask& bitmask, const TimeContext&) - { - if (m_attribute) - { - m_attribute->Set(bitmask); - } - } - - DataSet m_data; - DataSet::BindInterface m_bitmaskAttributeData; - BitmaskInterestAttribute::Ptr m_attribute; - }; - /////////////////////////////////////////////////////////////////// - - class TestPeerInfo - : public SessionEventBus::Handler - { - public: - TestPeerInfo() - : m_gridMate(nullptr) - , m_lanSearch(nullptr) - , m_session(nullptr) - , m_im(nullptr) - , m_bitmaskHandler(nullptr) - , m_num(0) - { - GridMate::ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - GridMate::ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - } - - void CreateTestReplica() - { - m_im = aznew InterestManager(); - InterestManagerDesc desc; - desc.m_rm = m_session->GetReplicaMgr(); - - m_im->Init(desc); - - m_bitmaskHandler = aznew BitmaskInterestHandler(); - m_im->RegisterHandler(m_bitmaskHandler); - - m_rule = m_bitmaskHandler->CreateRule(m_session->GetReplicaMgr()->GetLocalPeerId()); - m_rule->Set(1 << m_num); - - auto r = Replica::CreateReplica("InterestTestReplica"); - m_replica = CreateAndAttachReplicaChunk(r); - - // Initializing attribute - // Shifing all by one - peer0 will recv from peer1, peer2 will recv from peer2, peer2 will recv from peer0 - unsigned i = (m_num + 2) % Integ_InterestTest::k_numMachines; - m_replica->m_data.Set(m_num); - m_replica->m_bitmaskAttributeData.Set(1 << i); - - m_session->GetReplicaMgr()->AddPrimary(r); - } - - void UpdateAttribute() - { - // Shifing all by one - peer0 will recv from peer2, peer1 will recv from peer0, peer2 will recv from peer1 - unsigned i = (m_num + 1) % Integ_InterestTest::k_numMachines; - m_replica->m_bitmaskAttributeData.Set(1 << i); - m_replica->m_attribute->Set(1 << i); - } - - void DeleteAttribute() - { - m_replica->m_attribute = nullptr; - } - - void UpdateRule() - { - m_rule->Set(0xffff); - } - - void DeleteRule() - { - m_rule = nullptr; - } - - void CreateRule() - { - m_rule = m_bitmaskHandler->CreateRule(m_session->GetReplicaMgr()->GetLocalPeerId()); - m_rule->Set(0xffff); - } - - void OnSessionCreated(GridSession* session) override - { - m_session = session; - if (m_session->IsHost()) - { - CreateTestReplica(); - } - } - - void OnSessionJoined(GridSession* session) override - { - m_session = session; - CreateTestReplica(); - } - - void OnSessionDelete(GridSession* session) override - { - if (session == m_session) - { - m_rule = nullptr; - m_session = nullptr; - m_im->UnregisterHandler(m_bitmaskHandler); - delete m_bitmaskHandler; - delete m_im; - m_im = nullptr; - m_bitmaskHandler = nullptr; - } - } - - void OnSessionError(GridSession* session, const string& errorMsg) override - { - (void)session; - (void)errorMsg; - AZ_TracePrintf("GridMate", "Session error: %s\n", errorMsg.c_str()); - } - - IGridMate* m_gridMate; - GridSearch* m_lanSearch; - GridSession* m_session; - InterestManager* m_im; - BitmaskInterestHandler* m_bitmaskHandler; - - BitmaskInterestRule::Ptr m_rule; - unsigned m_num; - InterestTestChunk::Ptr m_replica; - }; - -public: - Integ_InterestTest() - { - ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - - ////////////////////////////////////////////////////////////////////////// - // Create all grid mates - m_peers[0].m_gridMate = m_gridMate; - m_peers[0].SessionEventBus::Handler::BusConnect(m_peers[0].m_gridMate); - m_peers[0].m_num = 0; - for (int i = 1; i < k_numMachines; ++i) - { - GridMateDesc desc; - m_peers[i].m_gridMate = GridMateCreate(desc); - AZ_TEST_ASSERT(m_peers[i].m_gridMate); - - m_peers[i].m_num = i; - m_peers[i].SessionEventBus::Handler::BusConnect(m_peers[i].m_gridMate); - } - ////////////////////////////////////////////////////////////////////////// - - for (int i = 0; i < k_numMachines; ++i) - { - // start the multiplayer service (session mgr, extra allocator, etc.) - StartGridMateService(m_peers[i].m_gridMate, SessionServiceDesc()); - AZ_TEST_ASSERT(LANSessionServiceBus::FindFirstHandler(m_peers[i].m_gridMate) != nullptr); - } - } - virtual ~Integ_InterestTest() - { - StopGridMateService(m_peers[0].m_gridMate); - - for (int i = 1; i < k_numMachines; ++i) - { - if (m_peers[i].m_gridMate) - { - m_peers[i].SessionEventBus::Handler::BusDisconnect(); - GridMateDestroy(m_peers[i].m_gridMate); - } - } - - // this will stop the first IGridMate which owns the memory allocators. - m_peers[0].SessionEventBus::Handler::BusDisconnect(); - } - - void run() - { - TestCarrierDesc carrierDesc; - carrierDesc.m_enableDisconnectDetection = false;// true; - carrierDesc.m_threadUpdateTimeMS = 10; - carrierDesc.m_familyType = Driver::BSD_AF_INET; - - - LANSessionParams sp; - sp.m_topology = ST_PEER_TO_PEER; - sp.m_numPublicSlots = 64; - sp.m_port = k_hostPort; - EBUS_EVENT_ID_RESULT(m_peers[k_host].m_session, m_peers[k_host].m_gridMate, LANSessionServiceBus, HostSession, sp, carrierDesc); - m_peers[k_host].m_session->GetReplicaMgr()->SetAutoBroadcast(false); - - int listenPort = k_hostPort; - for (int i = 0; i < k_numMachines; ++i) - { - if (i == k_host) - { - continue; - } - - LANSearchParams searchParams; - searchParams.m_serverPort = k_hostPort; - searchParams.m_listenPort = listenPort == k_hostPort ? 0 : ++listenPort; // first client will use ephemeral port, the rest specify return ports - searchParams.m_familyType = Driver::BSD_AF_INET; - EBUS_EVENT_ID_RESULT(m_peers[i].m_lanSearch, m_peers[i].m_gridMate, LANSessionServiceBus, StartGridSearch, searchParams); - } - - - static const int maxNumUpdates = 300; - int numUpdates = 0; - TimeStamp time = AZStd::chrono::system_clock::now(); - - while (numUpdates <= maxNumUpdates) - { - if (numUpdates == 100) - { - // Checking everybody received only one replica: - // peer0 -> rep1, peer1 -> rep2, peer2 -> rep0 - for (int i = 0; i < k_numMachines; ++i) - { - ReplicaId repId = m_peers[(i + 1) % k_numMachines].m_replica->GetReplicaId(); - auto repRecv = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(repRecv != nullptr); - - repId = m_peers[(i + 2) % k_numMachines].m_replica->GetReplicaId(); - auto repNotRecv = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(repNotRecv == nullptr); - - // rotating mask left - m_peers[i].UpdateAttribute(); - } - } - - if (numUpdates == 150) - { - // Checking everybody received only one replica: - // peer0 -> rep2, peer1 -> rep0, peer2 -> rep1 - for (int i = 0; i < k_numMachines; ++i) - { - ReplicaId repId = m_peers[(i + 2) % k_numMachines].m_replica->GetReplicaId(); - auto repRecv = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(repRecv != nullptr); - - repId = m_peers[(i + 1) % k_numMachines].m_replica->GetReplicaId(); - auto repNotRecv = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(repNotRecv == nullptr); - - // setting rules to accept all replicas - m_peers[i].UpdateRule(); - } - } - - if (numUpdates == 200) - { - // Checking everybody received all replicas - for (int i = 0; i < k_numMachines; ++i) - { - for (int j = 0; j < k_numMachines; ++j) - { - ReplicaId repId = m_peers[j].m_replica->GetReplicaId(); - auto rep = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(rep); - } - - // Deleting all attributes - m_peers[i].DeleteAttribute(); - } - } - - if (numUpdates == 250) - { - // Checking everybody lost all replicas (except primary) - for (int i = 0; i < k_numMachines; ++i) - { - for (int j = 0; j < k_numMachines; ++j) - { - if (i == j) - { - continue; - } - - ReplicaId repId = m_peers[j].m_replica->GetReplicaId(); - auto rep = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(rep == nullptr); - } - - // deleting all rules - m_peers[i].DeleteRule(); - } - } - - ////////////////////////////////////////////////////////////////////////// - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_gridMate) - { - m_peers[i].m_gridMate->Update(); - if (m_peers[i].m_session) - { - UpdateReplicas(m_peers[i].m_session->GetReplicaMgr(), m_peers[i].m_im); - } - } - } - Update(); - ////////////////////////////////////////////////////////////////////////// - - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_lanSearch && m_peers[i].m_lanSearch->IsDone()) - { - AZ_TEST_ASSERT(m_peers[i].m_lanSearch->GetNumResults() == 1); - JoinParams jp; - EBUS_EVENT_ID_RESULT(m_peers[i].m_session, m_peers[i].m_gridMate, LANSessionServiceBus, JoinSessionBySearchInfo, static_cast(*m_peers[i].m_lanSearch->GetResult(0)), jp, carrierDesc); - m_peers[i].m_session->GetReplicaMgr()->SetAutoBroadcast(false); - - m_peers[i].m_lanSearch->Release(); - m_peers[i].m_lanSearch = nullptr; - } - } - - ////////////////////////////////////////////////////////////////////////// - // Debug Info - TimeStamp now = AZStd::chrono::system_clock::now(); - if (AZStd::chrono::milliseconds(now - time).count() > 1000) - { - time = now; - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_session == nullptr) - { - continue; - } - - if (m_peers[i].m_session->IsHost()) - { - AZ_Printf("GridMate", "------ Host %d ------\n", i); - } - else - { - AZ_Printf("GridMate", "------ Client %d ------\n", i); - } - - AZ_Printf("GridMate", "Session %s Members: %d Host: %s Clock: %d\n", m_peers[i].m_session->GetId().c_str(), m_peers[i].m_session->GetNumberOfMembers(), m_peers[i].m_session->IsHost() ? "yes" : "no", m_peers[i].m_session->GetTime()); - for (unsigned int iMember = 0; iMember < m_peers[i].m_session->GetNumberOfMembers(); ++iMember) - { - GridMember* member = m_peers[i].m_session->GetMemberByIndex(iMember); - AZ_Printf("GridMate", " Member: %s(%s) Host: %s Local: %s\n", member->GetName().c_str(), member->GetId().ToString().c_str(), member->IsHost() ? "yes" : "no", member->IsLocal() ? "yes" : "no"); - } - AZ_Printf("GridMate", "\n"); - } - } - ////////////////////////////////////////////////////////////////////////// - - AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(30)); - numUpdates++; - } - } - - static const int k_numMachines = 3; - static const int k_host = 0; - static const int k_hostPort = 5450; - - TestPeerInfo m_peers[k_numMachines]; -}; - -/* - * Testing worse case performance of thousands of replicas and a few peers where all replicas/attributes change every frame - * and peers/rules change every frame as well. - */ -class LargeWorldTest - : public GridMateMPTestFixture -{ - /////////////////////////////////////////////////////////////////// - class LargeWorldTestChunk - : public ReplicaChunk - { - public: - GM_CLASS_ALLOCATOR(LargeWorldTestChunk); - - LargeWorldTestChunk() - : m_data("Data", 0) - , m_proximityAttributeData("LargeWorldAttributeData") - , m_attribute(nullptr) - { - } - - /////////////////////////////////////////////////////////////////// - typedef AZStd::intrusive_ptr Ptr; - static const char* GetChunkName() { return "LargeWorldTestChunk"; } - bool IsReplicaMigratable() override { return false; } - bool IsBroadcast() override - { - return false; - } - /////////////////////////////////////////////////////////////////// - - void OnReplicaActivate(const ReplicaContext& rc) override - { - /*if (!IsPrimary())*/ - /*{ - AZ_Printf("GridMate", "LargeWorldTestChunk::OnReplicaActivate repId=%08X(%s) fromPeerId=%08X localPeerId=%08X\n", - GetReplicaId(), - IsPrimary() ? "primary" : "proxy", - rc.m_peer ? rc.m_peer->GetId() : 0, - rc.m_rm->GetLocalPeerId()); - }*/ - - if (ProximityInterestHandler* ih = static_cast(rc.m_rm->GetUserContext(AZ_CRC("ProximityInterestHandler", 0x3a90b3e4)))) - { - m_attribute = ih->CreateAttribute(GetReplicaId()); - m_attribute->Set(m_proximityAttributeData.Get()); - } - } - - void OnReplicaDeactivate(const ReplicaContext& /*rc*/) override - { - m_attribute = nullptr; - } - - void ProximityHandler(const AZ::Aabb& bounds, const TimeContext&) - { - if (m_attribute) - { - m_attribute->Set(bounds); - } - } - - DataSet m_data; - DataSet::BindInterface m_proximityAttributeData; - ProximityInterestAttribute::Ptr m_attribute; - }; - /////////////////////////////////////////////////////////////////// - - struct LargeWorldParams - { - AZ::u32 index = 0; - - const float commonSize = 50; - const AZ::Aabb box = AZ::Aabb::CreateFromMinMax( - AZ::Vector3::CreateZero(), - AZ::Vector3::CreateOne() * commonSize); - const float commonStep = commonSize + 1; - }; - - static LargeWorldParams& GetWorldParams() - { - static LargeWorldParams worldParams; - return worldParams; - } - - /* - * Create a chain of boxes in spaces along X axis - */ - static AZ::Aabb CreateNextRuleSpace() - { - auto offset = GetWorldParams().commonStep * aznumeric_cast(GetWorldParams().index); - - float min[] = { offset, 0, 0 }; - float max[] = { - GetWorldParams().commonSize + offset, - GetWorldParams().commonSize, - GetWorldParams().commonSize }; - - auto bounds = AZ::Aabb::CreateFromMinMax( - AZ::Vector3::CreateFromFloat3(min), - AZ::Vector3::CreateFromFloat3(max)); - - GetWorldParams().index++; - return bounds; - } - - class LargeWorldTestPeerInfo - : public SessionEventBus::Handler - { - public: - LargeWorldTestPeerInfo() - : m_gridMate(nullptr) - , m_lanSearch(nullptr) - , m_session(nullptr) - , m_im(nullptr) - , m_proximityHandler(nullptr) - , m_num(0) - { - GridMate::ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - GridMate::ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - } - - ~LargeWorldTestPeerInfo() - { - SessionEventBus::Handler::BusDisconnect(); - } - - void CreateHostRuleHandler() - { - m_im = aznew InterestManager(); - InterestManagerDesc desc; - desc.m_rm = m_session->GetReplicaMgr(); - - m_im->Init(desc); - - m_proximityHandler = aznew ProximityInterestHandler(); - m_im->RegisterHandler(m_proximityHandler); - - m_rule = m_proximityHandler->CreateRule(m_session->GetReplicaMgr()->GetLocalPeerId()); - m_rule->Set(AZ::Aabb::CreateNull()); // host rule doesn't matter in this test - } - - void CreateRuleHandler() - { - m_im = aznew InterestManager(); - InterestManagerDesc desc; - desc.m_rm = m_session->GetReplicaMgr(); - - m_im->Init(desc); - - m_proximityHandler = aznew ProximityInterestHandler(); - m_im->RegisterHandler(m_proximityHandler); - - m_rule = m_proximityHandler->CreateRule(m_session->GetReplicaMgr()->GetLocalPeerId()); - m_rule->Set(CreateNextRuleSpace()); - } - - void CreateTestReplica(const AZ::Aabb& bounds) - { - auto r = Replica::CreateReplica("LargeWorldTestReplica"); - auto replica = CreateAndAttachReplicaChunk(r); - - // Initializing attribute - replica->m_data.Set(m_num); - replica->m_proximityAttributeData.Set(bounds); - - m_replicas.push_back(replica); - - m_session->GetReplicaMgr()->AddPrimary(r); - } - - void PopulateWorld() - { - AZ_Printf("GridMate", "LargeWorldTestChunk::PopulateWorld() starting...\n"); - - const float worldSizeInBoxes = 50; - const auto oneBox = AZ::Vector3::CreateOne(); - const auto thickness = 1; - for (float dx = 0; dx < worldSizeInBoxes; ++dx) - { - for (float dy = 0; dy < thickness; ++dy) - { - for (float dz = 0; dz < thickness; ++dz) - { - auto aabb = AZ::Aabb::CreateFromMinMax( - AZ::Vector3(50 * dx + 5, dy, dz), - AZ::Vector3(50 * dx + 5, dy, dz) + oneBox); - - CreateTestReplica(aabb); - } - } - } - - AZ_Printf("GridMate", "LargeWorldTestChunk::PopulateWorld() ... DONE\n"); - } - - void UpdateAttribute(LargeWorldTestChunk::Ptr replica) - { - if (replica && replica->m_attribute) - { - auto sameValue = replica->m_attribute->Get(); - - replica->m_proximityAttributeData.Set(sameValue); - replica->m_attribute->Set(sameValue); - } - } - - void UpdateRule() - { - // just make it dirty for now - if (m_rule) - { - auto sameValue = m_rule->Get(); - m_rule->Set(sameValue); - } - } - - void DeleteRule() - { - m_rule = nullptr; - } - - void OnSessionCreated(GridSession* session) override - { - m_session = session; - if (m_session->IsHost()) - { - CreateHostRuleHandler(); - PopulateWorld(); - } - } - - void OnSessionJoined(GridSession* session) override - { - m_session = session; - CreateRuleHandler(); - } - - void OnSessionDelete(GridSession* session) override - { - if (session == m_session) - { - m_rule = nullptr; - m_session = nullptr; - m_im->UnregisterHandler(m_proximityHandler); - delete m_proximityHandler; - delete m_im; - m_im = nullptr; - m_proximityHandler = nullptr; - } - } - - void OnSessionError(GridSession* session, const string& errorMsg) override - { - (void)session; - (void)errorMsg; - AZ_TracePrintf("GridMate", "Session error: %s\n", errorMsg.c_str()); - } - - IGridMate* m_gridMate; - GridSearch* m_lanSearch; - GridSession* m_session; - InterestManager* m_im; - ProximityInterestHandler* m_proximityHandler; - - ProximityInterestRule::Ptr m_rule; - unsigned m_num; - - AZStd::vector m_replicas; - }; - -public: - LargeWorldTest() : UnitTest::GridMateMPTestFixture(500u * 1024u * 1024u) // 500Mb - { - ReplicaChunkDescriptorTable::Get().RegisterChunkType(); - - ////////////////////////////////////////////////////////////////////////// - // Create all grid mates - m_peers[0].m_gridMate = m_gridMate; - m_peers[0].SessionEventBus::Handler::BusConnect(m_peers[0].m_gridMate); - m_peers[0].m_num = 0; - for (int i = 1; i < k_numMachines; ++i) - { - GridMateDesc desc; - m_peers[i].m_gridMate = GridMateCreate(desc); - AZ_TEST_ASSERT(m_peers[i].m_gridMate); - - m_peers[i].m_num = i; - m_peers[i].SessionEventBus::Handler::BusConnect(m_peers[i].m_gridMate); - } - ////////////////////////////////////////////////////////////////////////// - - for (int i = 0; i < k_numMachines; ++i) - { - // start the multiplayer service (session mgr, extra allocator, etc.) - StartGridMateService(m_peers[i].m_gridMate, SessionServiceDesc()); - AZ_TEST_ASSERT(LANSessionServiceBus::FindFirstHandler(m_peers[i].m_gridMate) != nullptr); - } - } - virtual ~LargeWorldTest() - { - StopGridMateService(m_peers[0].m_gridMate); - - for (int i = 1; i < k_numMachines; ++i) - { - if (m_peers[i].m_gridMate) - { - m_peers[i].SessionEventBus::Handler::BusDisconnect(); - GridMateDestroy(m_peers[i].m_gridMate); - } - } - - // this will stop the first IGridMate which owns the memory allocators. - m_peers[0].SessionEventBus::Handler::BusDisconnect(); - } - - void run() - { - g_PerfIM.Reset(); - g_PerfUpdatingAttributes.Reset(); - - TestCarrierDesc carrierDesc; - carrierDesc.m_enableDisconnectDetection = false; - carrierDesc.m_threadUpdateTimeMS = 10; - carrierDesc.m_familyType = Driver::BSD_AF_INET; - - LANSessionParams sp; - sp.m_topology = ST_PEER_TO_PEER; - sp.m_numPublicSlots = 64; - sp.m_port = k_hostPort; - EBUS_EVENT_ID_RESULT(m_peers[k_host].m_session, m_peers[k_host].m_gridMate, LANSessionServiceBus, HostSession, sp, carrierDesc); - m_peers[k_host].m_session->GetReplicaMgr()->SetAutoBroadcast(false); - - int listenPort = k_hostPort; - for (int i = 0; i < k_numMachines; ++i) - { - if (i == k_host) - { - continue; - } - - LANSearchParams searchParams; - searchParams.m_serverPort = k_hostPort; - searchParams.m_listenPort = listenPort == k_hostPort ? 0 : ++listenPort; // first client will use ephemeral port, the rest specify return ports - searchParams.m_familyType = Driver::BSD_AF_INET; - EBUS_EVENT_ID_RESULT(m_peers[i].m_lanSearch, m_peers[i].m_gridMate, LANSessionServiceBus, StartGridSearch, searchParams); - } - - - static const int maxNumUpdates = 300; - int numUpdates = 0; - TimeStamp time = AZStd::chrono::system_clock::now(); - - while (numUpdates <= maxNumUpdates) - { - g_PerfUpdatingAttributes.PreUpdate(); - for (LargeWorldTestChunk::Ptr replica : m_peers[0].m_replicas) - { - m_peers[0].UpdateAttribute(replica); - } - g_PerfUpdatingAttributes.PostUpdate(); - - for (auto peer : m_peers) - { - peer.UpdateRule(); - } - - //if (numUpdates == 100) - //{ - // // check how many replicas each client got - // for (AZ::u32 i = 1; i < k_numMachines; ++i) - // { - // AZ::u32 count = 0; - // for (auto& replica : m_peers[0].m_replicas) - // { - // ReplicaId repId = replica->GetReplicaId(); - // if (auto rep = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId)) - // { - // count++; - // } - // } - - // const AZ::Aabb& bounds = m_peers[i].m_rule->Get(); - - // AZ_Printf("GridMate", "Session %s Members: %d Bounds: %f.%f.%f-%f.%f.%f Replicas: %d\n", m_peers[i].m_session->GetId().c_str(), m_peers[i].m_session->GetNumberOfMembers(), - // static_cast(bounds.GetMin().GetX()), - // static_cast(bounds.GetMin().GetY()), - // static_cast(bounds.GetMin().GetZ()), - // static_cast(bounds.GetMax().GetX()), - // static_cast(bounds.GetMax().GetY()), - // static_cast(bounds.GetMax().GetZ()), count); - - // AZ_Assert(count == 1, "Should have at least some replicas to start with"); - // } - //} - - if (numUpdates == 200) - { - // Deleting all attributes - for (auto& replica : m_peers[0].m_replicas) - { - replica->m_attribute = nullptr; - } - } - - if (numUpdates == 250) - { - // Checking everybody lost all replicas (except primary) - for (int i = 0; i < k_numMachines; ++i) - { - /*for (int j = 0; j < k_numMachines; ++j) - { - if (i == j) - { - continue; - } - - ReplicaId repId = m_peers[j].m_replica->GetReplicaId(); - auto rep = m_peers[i].m_session->GetReplicaMgr()->FindReplica(repId); - AZ_TEST_ASSERT(rep == nullptr); - }*/ - - // deleting all rules - m_peers[i].DeleteRule(); - } - } - - ////////////////////////////////////////////////////////////////////////// - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_gridMate) - { - m_peers[i].m_gridMate->Update(); - if (m_peers[i].m_session) - { - UpdateReplicas(m_peers[i].m_session->GetReplicaMgr(), m_peers[i].m_im); - } - } - } - Update(); - ////////////////////////////////////////////////////////////////////////// - - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_lanSearch && m_peers[i].m_lanSearch->IsDone()) - { - AZ_TEST_ASSERT(m_peers[i].m_lanSearch->GetNumResults() == 1); - JoinParams jp; - EBUS_EVENT_ID_RESULT(m_peers[i].m_session, m_peers[i].m_gridMate, LANSessionServiceBus, JoinSessionBySearchInfo, static_cast(*m_peers[i].m_lanSearch->GetResult(0)), jp, carrierDesc); - m_peers[i].m_session->GetReplicaMgr()->SetAutoBroadcast(false); - - m_peers[i].m_lanSearch->Release(); - m_peers[i].m_lanSearch = nullptr; - } - } - - ////////////////////////////////////////////////////////////////////////// - // Debug Info - TimeStamp now = AZStd::chrono::system_clock::now(); - if (AZStd::chrono::milliseconds(now - time).count() > 1000) - { - time = now; - for (int i = 0; i < k_numMachines; ++i) - { - if (m_peers[i].m_session == nullptr) - { - continue; - } - - if (m_peers[i].m_session->IsHost()) - { - AZ_Printf("GridMate", "------ Host %d ------\n", i); - } - else - { - AZ_Printf("GridMate", "------ Client %d ------\n", i); - } - - AZ_Printf("GridMate", "Session %s Members: %d Host: %s Clock: %d\n", m_peers[i].m_session->GetId().c_str(), m_peers[i].m_session->GetNumberOfMembers(), m_peers[i].m_session->IsHost() ? "yes" : "no", m_peers[i].m_session->GetTime()); - for (unsigned int iMember = 0; iMember < m_peers[i].m_session->GetNumberOfMembers(); ++iMember) - { - GridMember* member = m_peers[i].m_session->GetMemberByIndex(iMember); - AZ_Printf("GridMate", " Member: %s(%s) Host: %s Local: %s\n", member->GetName().c_str(), member->GetId().ToString().c_str(), member->IsHost() ? "yes" : "no", member->IsLocal() ? "yes" : "no"); - } - AZ_Printf("GridMate", "\n"); - } - } - ////////////////////////////////////////////////////////////////////////// - - //AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(30)); - numUpdates++; - } - - auto averageFrame = g_PerfIM.GetAverageFrame(); - auto bestFrame = g_PerfIM.GetBestFrame(); - auto worstFrame = g_PerfIM.GetWorstFrame(); - auto frames = g_PerfIM.GetTotalFrames(); - AZ_Printf("GridMate", "Interest manager performance: average_frame = %f sec, frames = %d, best= %f sec, worst= %f sec\n", - averageFrame, frames, bestFrame, worstFrame); - - AZ_Printf("GridMate", "Updating attributes: average_frame = %f sec, frames = %d, best= %f sec, worst= %f sec\n", - g_PerfUpdatingAttributes.GetAverageFrame(), - g_PerfUpdatingAttributes.GetTotalFrames(), - g_PerfUpdatingAttributes.GetBestFrame(), - g_PerfUpdatingAttributes.GetWorstFrame()); - } - - static const int k_numMachines = 3; - static const int k_host = 0; - static const int k_hostPort = 5450; - - LargeWorldTestPeerInfo m_peers[k_numMachines]; -}; - -void PerfForInterestManager::Reset() -{ - m_frameCount = 0; - m_totalUpdateTime = 0; - m_slowestFrame = 0; - m_fastestFrame = 100.f; -} - -void PerfForInterestManager::PreUpdate() -{ - m_timer.Stamp(); -} - -void PerfForInterestManager::PostUpdate() -{ - auto frameTime = m_timer.StampAndGetDeltaTimeInSeconds(); - m_totalUpdateTime += frameTime; - m_frameCount++; - - m_slowestFrame = AZStd::max(m_slowestFrame, frameTime); - m_fastestFrame = AZStd::min(m_fastestFrame, frameTime); -} - -AZ::u32 PerfForInterestManager::GetTotalFrames() const -{ - return m_frameCount; -} - -float PerfForInterestManager::GetAverageFrame() const -{ - if (m_frameCount > 0) - { - return m_totalUpdateTime / m_frameCount; - } - - return 0; -} - -float PerfForInterestManager::GetWorstFrame() const -{ - return m_slowestFrame; -} - -float PerfForInterestManager::GetBestFrame() const -{ - return m_fastestFrame; -} - -class ProximityHandlerTests - : public GridMateMPTestFixture -{ -public: - struct xyz - { - float x, y, z; - }; - - static AZ::Aabb CreateBox(xyz min, float size) - { - return AZ::Aabb::CreateFromMinMax( - AZ::Vector3::CreateFromFloat3(&min.x), - AZ::Vector3::CreateFromFloat3(&min.x) + AZ::Vector3::CreateOne() * size); - } - - static void run() - { - SimpleFirstUpdate(); - SecondUpdateAfterNoChanges(); - SimpleOutsideOfRule(); - AttributeMovingOutsideOfRule(); - RuleMovingAndAttributeIsOut(); - RuleDestroyed(); - AttributeDestroyed(); - } - - static void RuleMovingAndAttributeIsOut() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - InterestMatchResult results = handler->GetLastResult(); - //ProximityInterestHandler::DebugPrint(results, "1st"); - - AZ_TEST_ASSERT(results[1].size() == 1); - AZ_TEST_ASSERT(results[1].find(100) != results[1].end()); - - // now move the attribute outside of the rule - rule1->Set(CreateBox({ 1000, 0, 0 }, 100)); - - handler->Update(); - results = handler->GetLastResult(); - //ProximityInterestHandler::DebugPrint(results, "2nd"); - - AZ_TEST_ASSERT(results[1].size() == 0); - } - - static void AttributeMovingOutsideOfRule() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - InterestMatchResult results = handler->GetLastResult(); - //ProximityInterestHandler::DebugPrint(results, "1st"); - - AZ_TEST_ASSERT(results[1].size() == 1); - AZ_TEST_ASSERT(results[1].find(100) != results[1].end()); - - // now move the attribute outside of the rule - attribute1->Set(CreateBox({ -1000, 0, 0 }, 10)); - - handler->Update(); - results = handler->GetLastResult(); - //ProximityInterestHandler::DebugPrint(results, "2nd"); - - AZ_TEST_ASSERT(results[1].size() == 0); - } - - static void SimpleFirstUpdate() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - - InterestMatchResult results = handler->GetLastResult(); - - //ProximityInterestHandler::PrintMatchResult(results, "test"); - - AZ_TEST_ASSERT(results[1].size() == 1); - AZ_TEST_ASSERT(results[1].find(100) != results[1].end()); - } - - static void SecondUpdateAfterNoChanges() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - handler->Update(); - - InterestMatchResult results = handler->GetLastResult(); - - //ProximityInterestHandler::PrintMatchResult(results, "test"); - - AZ_TEST_ASSERT(results.size() == 0); - } - - static void SimpleOutsideOfRule() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ -1000, 0, 0 }, 10)); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - - InterestMatchResult results = handler->GetLastResult(); - - //ProximityInterestHandler::PrintMatchResult(results, "test"); - - AZ_TEST_ASSERT(results.size() == 1); - AZ_TEST_ASSERT(results[1].size() == 0); - } - - static void RuleDestroyed() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - { - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - handler->Update(); - - InterestMatchResult results = handler->GetLastResult(); - AZ_TEST_ASSERT(results.size() == 1); - AZ_TEST_ASSERT(results[1].size() == 1); - } - - // rule1 should have been destroyed by now - - handler->Update(); - InterestMatchResult results = handler->GetLastResult(); - //ProximityInterestHandler::PrintMatchResult(results, "last"); - - AZ_TEST_ASSERT(results.size() == 1); - AZ_TEST_ASSERT(results[1].size() == 0); - } - - static void AttributeDestroyed() - { - AZStd::unique_ptr handler(aznew ProximityInterestHandler()); - - auto rule1 = handler->CreateRule(100); - rule1->Set(CreateBox({ 0, 0, 0 }, 100)); - - { - auto attribute1 = handler->CreateAttribute(1); - attribute1->Set(CreateBox({ 0, 0, 0 }, 10)); - - handler->Update(); - - InterestMatchResult results = handler->GetLastResult(); - AZ_TEST_ASSERT(results.size() == 1); - AZ_TEST_ASSERT(results[1].size() == 1); - } - - // attribute1 should have been destroyed by now, but it will show up once to remove it from affected peers - handler->Update(); - InterestMatchResult results = handler->GetLastResult(); - results.PrintMatchResult("last"); - - AZ_TEST_ASSERT(results.size() == 1); - AZ_TEST_ASSERT(results[1].size() == 0); - - // and now attribute1 should not show up in the changes - handler->Update(); - results = handler->GetLastResult(); - results.PrintMatchResult("last"); - - AZ_TEST_ASSERT(results.size() == 0); - } -}; - -}; // namespace UnitTest - -GM_TEST_SUITE(InterestSuite) - GM_TEST(Integ_InterestTest); -#if AZ_TRAIT_GRIDMATE_TEST_EXCLUDE_LARGEWORLDTEST != 0 - GM_TEST(LargeWorldTest); -#endif - GM_TEST(ProximityHandlerTests); -GM_TEST_SUITE_END() diff --git a/Code/Framework/GridMate/Tests/gridmate_test_files.cmake b/Code/Framework/GridMate/Tests/gridmate_test_files.cmake index a8b1dbd120..90ccbbf9a9 100644 --- a/Code/Framework/GridMate/Tests/gridmate_test_files.cmake +++ b/Code/Framework/GridMate/Tests/gridmate_test_files.cmake @@ -24,5 +24,4 @@ set(FILES StreamSocketDriverTests.cpp CarrierStreamSocketDriverTests.cpp Carrier.cpp - Interest.cpp ) diff --git a/Code/LauncherUnified/Launcher.cpp b/Code/LauncherUnified/Launcher.cpp index c73cdd1981..09a9e2f990 100644 --- a/Code/LauncherUnified/Launcher.cpp +++ b/Code/LauncherUnified/Launcher.cpp @@ -46,20 +46,25 @@ extern "C" void CreateStaticModules(AZStd::vector& modulesOut); # define REMOTE_ASSET_PROCESSOR #endif +void CVar_OnViewportPosition(const AZ::Vector2& value); + namespace { - void OnViewportResize(const AZ::Vector2& value); + void CVar_OnViewportResize(const AZ::Vector2& value); - AZ_CVAR(AZ::Vector2, r_viewportSize, AZ::Vector2::CreateZero(), OnViewportResize, AZ::ConsoleFunctorFlags::DontReplicate, + AZ_CVAR(AZ::Vector2, r_viewportSize, AZ::Vector2::CreateZero(), CVar_OnViewportResize, AZ::ConsoleFunctorFlags::DontReplicate, "The default size for the launcher viewport, 0 0 means full screen"); - void OnViewportResize(const AZ::Vector2& value) + void CVar_OnViewportResize(const AZ::Vector2& value) { AzFramework::NativeWindowHandle windowHandle = nullptr; AzFramework::WindowSystemRequestBus::BroadcastResult(windowHandle, &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle); AzFramework::WindowSize newSize = AzFramework::WindowSize(aznumeric_cast(value.GetX()), aznumeric_cast(value.GetY())); AzFramework::WindowRequestBus::Broadcast(&AzFramework::WindowRequestBus::Events::ResizeClientArea, newSize); } + + AZ_CVAR(AZ::Vector2, r_viewportPos, AZ::Vector2::CreateZero(), CVar_OnViewportPosition, AZ::ConsoleFunctorFlags::DontReplicate, + "The default position for the launcher viewport, 0 0 means top left corner of your main desktop"); void ExecuteConsoleCommandFile(AzFramework::Application& application) { diff --git a/Code/LauncherUnified/Platform/Android/Launcher_Android.cpp b/Code/LauncherUnified/Platform/Android/Launcher_Android.cpp index 6455373e58..fa26b9d204 100644 --- a/Code/LauncherUnified/Platform/Android/Launcher_Android.cpp +++ b/Code/LauncherUnified/Platform/Android/Launcher_Android.cpp @@ -433,3 +433,5 @@ void android_main(android_app* appState) MAIN_EXIT_FAILURE(appState, GetReturnCodeString(status)); } } + +void CVar_OnViewportPosition([[maybe_unused]] const AZ::Vector2& value) {} diff --git a/Code/LauncherUnified/Platform/Linux/Launcher_Linux.cpp b/Code/LauncherUnified/Platform/Linux/Launcher_Linux.cpp index 0c422877b1..3d283ce77f 100644 --- a/Code/LauncherUnified/Platform/Linux/Launcher_Linux.cpp +++ b/Code/LauncherUnified/Platform/Linux/Launcher_Linux.cpp @@ -113,3 +113,5 @@ int main(int argc, char** argv) return static_cast(status); } + +void CVar_OnViewportPosition([[maybe_unused]] const AZ::Vector2& value) {} diff --git a/Code/LauncherUnified/Platform/Mac/Launcher_Mac.mm b/Code/LauncherUnified/Platform/Mac/Launcher_Mac.mm index 1a491d66ad..2886bb16b1 100644 --- a/Code/LauncherUnified/Platform/Mac/Launcher_Mac.mm +++ b/Code/LauncherUnified/Platform/Mac/Launcher_Mac.mm @@ -63,3 +63,5 @@ int main(int argc, char* argv[]) } #endif // AZ_TESTS_ENABLED + +void CVar_OnViewportPosition([[maybe_unused]] const AZ::Vector2& value) {} diff --git a/Code/LauncherUnified/Platform/Windows/Launcher_Windows.cpp b/Code/LauncherUnified/Platform/Windows/Launcher_Windows.cpp index 8f8d310475..494779dcf8 100644 --- a/Code/LauncherUnified/Platform/Windows/Launcher_Windows.cpp +++ b/Code/LauncherUnified/Platform/Windows/Launcher_Windows.cpp @@ -69,3 +69,14 @@ int APIENTRY WinMain([[maybe_unused]] HINSTANCE hInstance, [[maybe_unused]] HINS return static_cast(status); } + +void CVar_OnViewportPosition(const AZ::Vector2& value) +{ + if (HWND windowHandle = GetActiveWindow()) + { + SetWindowPos(windowHandle, nullptr, + value.GetX(), + value.GetY(), + 0, 0, SWP_NOOWNERZORDER | SWP_NOSIZE); + } +} diff --git a/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm b/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm index 52c616fe64..05857842a7 100644 --- a/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm +++ b/Code/LauncherUnified/Platform/iOS/Launcher_iOS.mm @@ -23,3 +23,5 @@ int main(int argc, char* argv[]) [pool release]; return 0; } + +void CVar_OnViewportPosition([[maybe_unused]] const AZ::Vector2& value) {} diff --git a/Code/LauncherUnified/launcher_generator.cmake b/Code/LauncherUnified/launcher_generator.cmake index 36cd3c5899..5fa0f7a0e3 100644 --- a/Code/LauncherUnified/launcher_generator.cmake +++ b/Code/LauncherUnified/launcher_generator.cmake @@ -196,6 +196,16 @@ function(ly_delayed_generate_static_modules_inl) ly_get_gem_load_dependencies(all_game_gem_dependencies ${project_name}.GameLauncher) foreach(game_gem_dependency ${all_game_gem_dependencies}) + # Sometimes, a gem's Client variant may be an interface library + # which dependes on multiple gem targets. The interface libraries + # should be skipped; the real dependencies of the interface will be processed + if(TARGET ${game_gem_dependency}) + get_target_property(target_type ${game_gem_dependency} TYPE) + if(${target_type} STREQUAL "INTERFACE_LIBRARY") + continue() + endif() + endif() + # To match the convention on how gems targets vs gem modules are named, # we remove the ".Static" from the suffix # Replace "." with "_" @@ -224,6 +234,14 @@ function(ly_delayed_generate_static_modules_inl) list(APPEND all_server_gem_dependencies ${server_gem_load_dependencies} ${server_gem_dependency}) endforeach() foreach(server_gem_dependency ${all_server_gem_dependencies}) + # Skip interface libraries + if(TARGET ${server_gem_dependency}) + get_target_property(target_type ${server_gem_dependency} TYPE) + if(${target_type} STREQUAL "INTERFACE_LIBRARY") + continue() + endif() + endif() + # Replace "." with "_" string(REPLACE "." "_" server_gem_dependency ${server_gem_dependency}) diff --git a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp index 0b23739bb8..1bc8b9d324 100644 --- a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp +++ b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.cpp @@ -1,24 +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. -* -*/ + * 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 "EditorDefs.h" #include "AzAssetBrowserWindow.h" // AzToolsFramework +#include #include #include -#include #include +#include +#include // AzQtComponents #include @@ -31,6 +33,7 @@ AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING #include AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING +AZ_CVAR_EXTERNED(bool, ed_useNewAssetBrowserTableView); class ListenerForShowAssetEditorEvent : public QObject @@ -66,32 +69,75 @@ AzAssetBrowserWindow::AzAssetBrowserWindow(QWidget* parent) : QWidget(parent) , m_ui(new Ui::AzAssetBrowserWindowClass()) , m_filterModel(new AzToolsFramework::AssetBrowser::AssetBrowserFilterModel(parent)) + , m_tableModel(new AzToolsFramework::AssetBrowser::AssetBrowserTableModel(parent)) { m_ui->setupUi(this); m_ui->m_searchWidget->Setup(true, true); - using namespace AzToolsFramework::AssetBrowser; - AssetBrowserComponentRequestBus::BroadcastResult(m_assetBrowserModel, &AssetBrowserComponentRequests::GetAssetBrowserModel); + namespace AzAssetBrowser = AzToolsFramework::AssetBrowser; + + AzAssetBrowser::AssetBrowserComponentRequestBus::BroadcastResult(m_assetBrowserModel, &AzAssetBrowser::AssetBrowserComponentRequests::GetAssetBrowserModel); AZ_Assert(m_assetBrowserModel, "Failed to get filebrowser model"); m_filterModel->setSourceModel(m_assetBrowserModel); m_filterModel->SetFilter(m_ui->m_searchWidget->GetFilter()); + m_ui->m_viewSwitcherCheckBox->setVisible(false); + m_ui->m_assetBrowserTableViewWidget->setVisible(false); + if (ed_useNewAssetBrowserTableView) + { + m_ui->m_viewSwitcherCheckBox->setVisible(true); + m_tableModel->setFilterRole(Qt::DisplayRole); + m_tableModel->setSourceModel(m_filterModel.data()); + m_ui->m_assetBrowserTableViewWidget->setModel(m_tableModel.data()); + connect( + m_filterModel.data(), &AzAssetBrowser::AssetBrowserFilterModel::filterChanged, m_tableModel.data(), + &AzAssetBrowser::AssetBrowserTableModel::UpdateTableModelMaps); + connect( + m_ui->m_assetBrowserTableViewWidget, &AzAssetBrowser::AssetBrowserTableView::selectionChangedSignal, this, + &AzAssetBrowserWindow::SelectionChangedSlot); + connect( + m_ui->m_assetBrowserTableViewWidget, &QAbstractItemView::doubleClicked, this, + &AzAssetBrowserWindow::DoubleClickedItem); + connect( + m_ui->m_assetBrowserTableViewWidget, &AzAssetBrowser::AssetBrowserTableView::ClearStringFilter, m_ui->m_searchWidget, + &AzAssetBrowser::SearchWidget::ClearStringFilter); + connect( + m_ui->m_assetBrowserTableViewWidget, &AzAssetBrowser::AssetBrowserTableView::ClearTypeFilter, m_ui->m_searchWidget, + &AzAssetBrowser::SearchWidget::ClearTypeFilter); + + m_ui->m_assetBrowserTableViewWidget->SetName("AssetBrowserTableView_main"); + + connect(m_filterModel.data(), &AzAssetBrowser::AssetBrowserFilterModel::stringFilterPopulated, this, &AzAssetBrowserWindow::SwitchDisplayView); + connect(m_ui->m_viewSwitcherCheckBox, &QCheckBox::stateChanged, this, &AzAssetBrowserWindow::LockToDefaultView); + } + m_ui->m_assetBrowserTreeViewWidget->setModel(m_filterModel.data()); - connect(m_ui->m_searchWidget->GetFilter().data(), &AssetBrowserEntryFilter::updatedSignal, - m_filterModel.data(), &AssetBrowserFilterModel::filterUpdatedSlot); - connect(m_filterModel.data(), &AssetBrowserFilterModel::filterChanged, this, [this]() - { - const bool hasFilter = !m_ui->m_searchWidget->GetFilterString().isEmpty(); - const bool selectFirstFilteredIndex = false; - m_ui->m_assetBrowserTreeViewWidget->UpdateAfterFilter(hasFilter, selectFirstFilteredIndex); - }); - connect(m_ui->m_assetBrowserTreeViewWidget, &AssetBrowserTreeView::selectionChangedSignal, - this, &AzAssetBrowserWindow::SelectionChangedSlot); + connect( + m_ui->m_searchWidget->GetFilter().data(), &AzAssetBrowser::AssetBrowserEntryFilter::updatedSignal, m_filterModel.data(), + &AzAssetBrowser::AssetBrowserFilterModel::filterUpdatedSlot); + connect( + m_filterModel.data(), &AzAssetBrowser::AssetBrowserFilterModel::filterChanged, this, + [this]() + { + const bool hasFilter = !m_ui->m_searchWidget->GetFilterString().isEmpty(); + const bool selectFirstFilteredIndex = false; + m_ui->m_assetBrowserTreeViewWidget->UpdateAfterFilter(hasFilter, selectFirstFilteredIndex); + }); + + connect( + m_ui->m_assetBrowserTreeViewWidget, &AzAssetBrowser::AssetBrowserTreeView::selectionChangedSignal, this, + &AzAssetBrowserWindow::SelectionChangedSlot); + connect(m_ui->m_assetBrowserTreeViewWidget, &QAbstractItemView::doubleClicked, this, &AzAssetBrowserWindow::DoubleClickedItem); - connect(m_ui->m_assetBrowserTreeViewWidget, &AssetBrowserTreeView::ClearStringFilter, m_ui->m_searchWidget, &SearchWidget::ClearStringFilter); - connect(m_ui->m_assetBrowserTreeViewWidget, &AssetBrowserTreeView::ClearTypeFilter, m_ui->m_searchWidget, &SearchWidget::ClearTypeFilter); + connect( + m_ui->m_assetBrowserTreeViewWidget, &AzAssetBrowser::AssetBrowserTreeView::ClearStringFilter, m_ui->m_searchWidget, + &AzAssetBrowser::SearchWidget::ClearStringFilter); + connect( + m_ui->m_assetBrowserTreeViewWidget, &AzAssetBrowser::AssetBrowserTreeView::ClearTypeFilter, m_ui->m_searchWidget, + &AzAssetBrowser::SearchWidget::ClearTypeFilter); + m_ui->m_assetBrowserTreeViewWidget->SetName("AssetBrowserTreeView_main"); } @@ -117,7 +163,10 @@ QObject* AzAssetBrowserWindow::createListenerForShowAssetEditorEvent(QObject* pa void AzAssetBrowserWindow::UpdatePreview() const { - auto selectedAssets = m_ui->m_assetBrowserTreeViewWidget->GetSelectedAssets(); + const auto& selectedAssets = m_ui->m_assetBrowserTreeViewWidget->isVisible() + ? m_ui->m_assetBrowserTreeViewWidget->GetSelectedAssets() + : m_ui->m_assetBrowserTableViewWidget->GetSelectedAssets(); + if (selectedAssets.size() != 1) { m_ui->m_previewerFrame->Clear(); @@ -148,8 +197,6 @@ static void ExpandTreeToIndex(QTreeView* treeView, const QModelIndex& index) void AzAssetBrowserWindow::SelectAsset(const QString& assetPath) { - using namespace AzToolsFramework::AssetBrowser; - QModelIndex index = m_assetBrowserModel->findIndex(assetPath); if (index.isValid()) { @@ -161,18 +208,21 @@ void AzAssetBrowserWindow::SelectAsset(const QString& assetPath) // interferes with the update from the select and expand, and if you don't // queue it, the tree doesn't expand reliably. - QTimer::singleShot(0, this, [this, filteredIndex = index] { - // the treeview has a filter model so we have to backwards go from that - QModelIndex index = m_filterModel->mapFromSource(filteredIndex); + QTimer::singleShot( + 0, this, + [this, filteredIndex = index] + { + // the treeview has a filter model so we have to backwards go from that + QModelIndex index = m_filterModel->mapFromSource(filteredIndex); - QTreeView* treeView = m_ui->m_assetBrowserTreeViewWidget; - ExpandTreeToIndex(treeView, index); + QTreeView* treeView = m_ui->m_assetBrowserTreeViewWidget; + ExpandTreeToIndex(treeView, index); - treeView->scrollTo(index); - treeView->setCurrentIndex(index); + treeView->scrollTo(index); + treeView->setCurrentIndex(index); - treeView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); - }); + treeView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); + }); } } @@ -185,31 +235,34 @@ void AzAssetBrowserWindow::SelectionChangedSlot(const QItemSelection& /*selected // just becuase on some OS clicking once is activation. void AzAssetBrowserWindow::DoubleClickedItem([[maybe_unused]] const QModelIndex& element) { - using namespace AzToolsFramework; - using namespace AzToolsFramework::AssetBrowser; - // assumption: Double clicking an item selects it before telling us we double clicked it. - auto selectedAssets = m_ui->m_assetBrowserTreeViewWidget->GetSelectedAssets(); - for (const AssetBrowserEntry* entry : selectedAssets) + namespace AzAssetBrowser = AzToolsFramework::AssetBrowser; + + const auto& selectedAssets = m_ui->m_assetBrowserTreeViewWidget->isVisible() + ? m_ui->m_assetBrowserTreeViewWidget->GetSelectedAssets() + : m_ui->m_assetBrowserTableViewWidget->GetSelectedAssets(); + + for (const AzAssetBrowser::AssetBrowserEntry* entry : selectedAssets) { AZ::Data::AssetId assetIdToOpen; AZStd::string fullFilePath; - if (const ProductAssetBrowserEntry* productEntry = azrtti_cast(entry)) + if (const AzAssetBrowser::ProductAssetBrowserEntry* productEntry = azrtti_cast(entry)) { assetIdToOpen = productEntry->GetAssetId(); fullFilePath = entry->GetFullPath(); } - else if (const SourceAssetBrowserEntry* sourceEntry = azrtti_cast(entry)) + else if (const AzAssetBrowser::SourceAssetBrowserEntry* sourceEntry = azrtti_cast(entry)) { // manufacture an empty AssetID with the source's UUID assetIdToOpen = AZ::Data::AssetId(sourceEntry->GetSourceUuid(), 0); fullFilePath = entry->GetFullPath(); } - + bool handledBySomeone = false; if (assetIdToOpen.IsValid()) { - AssetBrowserInteractionNotificationBus::Broadcast(&AssetBrowserInteractionNotifications::OpenAssetInAssociatedEditor, assetIdToOpen, handledBySomeone); + AzAssetBrowser::AssetBrowserInteractionNotificationBus::Broadcast( + &AzAssetBrowser::AssetBrowserInteractionNotifications::OpenAssetInAssociatedEditor, assetIdToOpen, handledBySomeone); } if (!handledBySomeone && !fullFilePath.empty()) @@ -217,7 +270,27 @@ void AzAssetBrowserWindow::DoubleClickedItem([[maybe_unused]] const QModelIndex& AzAssetBrowserRequestHandler::OpenWithOS(fullFilePath); } } +} +void AzAssetBrowserWindow::SwitchDisplayView(bool state) +{ + m_ui->m_assetBrowserTableViewWidget->setVisible(state); + m_ui->m_assetBrowserTreeViewWidget->setVisible(!state); +} + +void AzAssetBrowserWindow::LockToDefaultView(bool state) +{ + using AzToolsFramework::AssetBrowser::AssetBrowserFilterModel; + SwitchDisplayView(!state); + if (state == true) + { + disconnect( + m_filterModel.data(), &AssetBrowserFilterModel::stringFilterPopulated, this, &AzAssetBrowserWindow::SwitchDisplayView); + } + else + { + connect(m_filterModel.data(), &AssetBrowserFilterModel::stringFilterPopulated, this, &AzAssetBrowserWindow::SwitchDisplayView); + } } #include diff --git a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.h b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.h index 263712dfe2..57a5371ab9 100644 --- a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.h +++ b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.h @@ -29,7 +29,9 @@ namespace AzToolsFramework namespace AssetBrowser { class AssetBrowserFilterModel; + class AssetBrowserTableModel; class AssetBrowserModel; + class AssetBrowserTableFilterModel; } } @@ -53,6 +55,7 @@ private: QScopedPointer m_ui; QScopedPointer m_filterModel; + QScopedPointer m_tableModel; AzToolsFramework::AssetBrowser::AssetBrowserModel* m_assetBrowserModel; void UpdatePreview() const; @@ -60,6 +63,8 @@ private: private Q_SLOTS: void SelectionChangedSlot(const QItemSelection& selected, const QItemSelection& deselected) const; void DoubleClickedItem(const QModelIndex& element); + void SwitchDisplayView(bool state); + void LockToDefaultView(bool state); }; extern const char* AZ_ASSET_BROWSER_PREVIEW_NAME; diff --git a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui index d6a6b4b8df..37eb1521f3 100644 --- a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui +++ b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserWindow.ui @@ -65,6 +65,13 @@ + + + + Switch View + + + @@ -107,6 +114,49 @@ 0 + + + + + 0 + 0 + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::DragOnly + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + false + + + true + + + false + + + true + + + false + + + @@ -162,6 +212,11 @@
AzToolsFramework/AssetBrowser/Previewer/PreviewerFrame.h
1 + + AzToolsFramework::AssetBrowser::AssetBrowserTableView + QTableView +
AzToolsFramework/AssetBrowser/Views/AssetBrowserTableView.h
+
diff --git a/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp b/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp index 502f396bf1..faabaebb05 100644 --- a/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp +++ b/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp @@ -715,10 +715,11 @@ QMenu* LevelEditorMenuHandler::CreateViewMenu() { return view.IsViewportPane(); }); -#endif viewportViewsMenuWrapper.AddSeparator(); +#endif + if (CViewManager::IsMultiViewportEnabled()) { viewportViewsMenuWrapper.AddAction(ID_VIEW_CONFIGURELAYOUT); diff --git a/Code/Sandbox/Editor/CryEdit.cpp b/Code/Sandbox/Editor/CryEdit.cpp index 4a0b9cdc3d..4169bf93a8 100644 --- a/Code/Sandbox/Editor/CryEdit.cpp +++ b/Code/Sandbox/Editor/CryEdit.cpp @@ -53,6 +53,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include // AzFramework #include @@ -356,6 +357,8 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(LPCTSTR lpszFileName, BOOL bAddToM for (int i = idStart; i <= idEnd; ++i) \ ON_COMMAND(i, method); +AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once); + void CCryEditApp::RegisterActionHandlers() { ON_COMMAND(ID_APP_ABOUT, OnAppAbout) @@ -376,6 +379,10 @@ void CCryEditApp::RegisterActionHandlers() ON_COMMAND(ID_EDIT_FETCH, OnEditFetch) ON_COMMAND(ID_FILE_EXPORTTOGAMENOSURFACETEXTURE, OnFileExportToGameNoSurfaceTexture) ON_COMMAND(ID_VIEW_SWITCHTOGAME, OnViewSwitchToGame) + MainWindow::instance()->GetActionManager()->RegisterActionHandler(ID_VIEW_SWITCHTOGAME_FULLSCREEN, [this]() { + ed_previewGameInFullscreen_once = true; + OnViewSwitchToGame(); + }); ON_COMMAND(ID_MOVE_OBJECT, OnMoveObject) ON_COMMAND(ID_RENAME_OBJ, OnRenameObj) ON_COMMAND(ID_EDITMODE_MOVE, OnEditmodeMove) @@ -2886,7 +2893,7 @@ void CCryEditApp::OpenProjectManager(const AZStd::string& screen) { // provide the current project path for in case we want to update the project AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath(); - const AZStd::string commandLineOptions = AZStd::string::format(" --screen %s --project_path %s", screen.c_str(), projectPath.c_str()); + const AZStd::string commandLineOptions = AZStd::string::format(" --screen %s --project-path %s", screen.c_str(), projectPath.c_str()); bool launchSuccess = AzFramework::ProjectManager::LaunchProjectManager(commandLineOptions); if (!launchSuccess) { diff --git a/Code/Sandbox/Editor/EditorViewportSettings.cpp b/Code/Sandbox/Editor/EditorViewportSettings.cpp index dbfd3ea4ed..02e280b8d1 100644 --- a/Code/Sandbox/Editor/EditorViewportSettings.cpp +++ b/Code/Sandbox/Editor/EditorViewportSettings.cpp @@ -14,6 +14,7 @@ #include #include +#include #include namespace SandboxEditor @@ -56,6 +57,39 @@ namespace SandboxEditor return value; } + struct EditorViewportSettingsCallbacksImpl : public EditorViewportSettingsCallbacks + { + EditorViewportSettingsCallbacksImpl() + { + if (auto* registry = AZ::SettingsRegistry::Get()) + { + using AZ::SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual; + + m_notifyEventHandler = registry->RegisterNotifier( + [this](const AZStd::string_view path, [[maybe_unused]] const AZ::SettingsRegistryInterface::Type type) + { + if (IsPathAncestorDescendantOrEqual(GridSnappingSetting, path)) + { + m_gridSnappingChanged.Signal(GridSnappingEnabled()); + } + }); + } + } + + void SetGridSnappingChangedEvent(GridSnappingChangedEvent::Handler& handler) override + { + handler.Connect(m_gridSnappingChanged); + } + + GridSnappingChangedEvent m_gridSnappingChanged; + AZ::SettingsRegistryInterface::NotifyEventHandler m_notifyEventHandler; + }; + + AZStd::unique_ptr CreateEditorViewportSettingsCallbacks() + { + return AZStd::make_unique(); + } + bool GridSnappingEnabled() { return GetRegistry(GridSnappingSetting, false); diff --git a/Code/Sandbox/Editor/EditorViewportSettings.h b/Code/Sandbox/Editor/EditorViewportSettings.h index a2f80d196e..d3a082c095 100644 --- a/Code/Sandbox/Editor/EditorViewportSettings.h +++ b/Code/Sandbox/Editor/EditorViewportSettings.h @@ -14,8 +14,27 @@ #include +#include +#include + namespace SandboxEditor { + using GridSnappingChangedEvent = AZ::Event; + + //! Set callbacks to listen for editor settings change events. + class EditorViewportSettingsCallbacks + { + public: + virtual ~EditorViewportSettingsCallbacks() = default; + + virtual void SetGridSnappingChangedEvent(GridSnappingChangedEvent::Handler& handler) = 0; + }; + + //! Create an instance of EditorViewportSettingsCallbacks + //! Note: EditorViewportSettingsCallbacks is implemented in EditorViewportSettings.cpp - a change + //! event will fire when a value in the settings registry (editorpreferences.setreg) is modified. + SANDBOX_API AZStd::unique_ptr CreateEditorViewportSettingsCallbacks(); + SANDBOX_API bool GridSnappingEnabled(); SANDBOX_API void SetGridSnapping(bool enabled); diff --git a/Code/Sandbox/Editor/EditorViewportWidget.cpp b/Code/Sandbox/Editor/EditorViewportWidget.cpp index 5a444f0521..699cd7acb9 100644 --- a/Code/Sandbox/Editor/EditorViewportWidget.cpp +++ b/Code/Sandbox/Editor/EditorViewportWidget.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include // AzFramework @@ -75,13 +76,14 @@ #include "EditorPreferencesPageGeneral.h" #include "ViewportManipulatorController.h" #include "LegacyViewportCameraController.h" -#include "EditorViewportSettings.h" #include "ViewPane.h" #include "CustomResolutionDlg.h" #include "AnimationContext.h" #include "Objects/SelectionGroup.h" #include "Core/QtEditorApplication.h" +#include "MainWindow.h" +#include "LayoutWnd.h" // ComponentEntityEditorPlugin #include @@ -513,18 +515,28 @@ void EditorViewportWidget::Update() // Disable rendering to avoid recursion into Update() PushDisableRendering(); + + //get debug display interface for the viewport + AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; + AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, GetViewportId()); + AZ_Assert(debugDisplayBus, "Invalid DebugDisplayRequestBus."); + + AzFramework::DebugDisplayRequests* debugDisplay = + AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); + + // draw debug visualizations - if (m_debugDisplay) + if (debugDisplay) { - const AZ::u32 prevState = m_debugDisplay->GetState(); - m_debugDisplay->SetState( + const AZ::u32 prevState = debugDisplay->GetState(); + debugDisplay->SetState( e_Mode3D | e_AlphaBlended | e_FillModeSolid | e_CullModeBack | e_DepthWriteOn | e_DepthTestOn); AzFramework::EntityDebugDisplayEventBus::Broadcast( &AzFramework::EntityDebugDisplayEvents::DisplayEntityViewport, - AzFramework::ViewportInfo{ GetViewportId() }, *m_debugDisplay); + AzFramework::ViewportInfo{ GetViewportId() }, *debugDisplay); - m_debugDisplay->SetState(prevState); + debugDisplay->SetState(prevState); } QtViewport::Update(); @@ -694,6 +706,11 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) } } SetCurrentCursor(STD_CURSOR_GAME); + + if (ShouldPreviewFullscreen()) + { + StartFullscreenPreview(); + } } if (m_renderViewport) @@ -712,6 +729,11 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event) m_bInOrbitMode = false; m_bInZoomMode = false; + if (m_inFullscreenPreview) + { + StopFullscreenPreview(); + } + RestoreViewportAfterGameMode(); } @@ -1368,11 +1390,35 @@ void EditorViewportWidget::SetViewportId(int id) { CViewport::SetViewportId(id); + // Clear the cached debugdisplay pointer. we're about to delete that render viewport, and deleting the render + // viewport invalidates the debugdisplay. + m_debugDisplay = nullptr; + + // First delete any existing layout + // This also deletes any existing render viewport widget (since it will be added to the layout) + // Below is the typical method of clearing a QLayout, see e.g. https://doc.qt.io/qt-5/qlayout.html#takeAt + if (QLayout* thisLayout = layout()) + { + QLayoutItem* item; + while ((item = thisLayout->takeAt(0)) != nullptr) + { + if (QWidget* widget = item->widget()) + { + delete widget; + } + thisLayout->removeItem(item); + delete item; + } + delete thisLayout; + } + // Now that we have an ID, we can initialize our viewport. m_renderViewport = new AtomToolsFramework::RenderViewportWidget(this, false); if (!m_renderViewport->InitializeViewportContext(id)) { AZ_Warning("EditorViewportWidget", false, "Failed to initialize RenderViewportWidget's ViewportContext"); + delete m_renderViewport; + m_renderViewport = nullptr; return; } auto viewportContext = m_renderViewport->GetViewportContext(); @@ -1403,6 +1449,17 @@ void EditorViewportWidget::SetViewportId(int id) { SetAsActiveViewport(); } + + m_editorViewportSettingsCallbacks = SandboxEditor::CreateEditorViewportSettingsCallbacks(); + + m_gridSnappingHandler = SandboxEditor::GridSnappingChangedEvent::Handler( + [id](const bool snapping) + { + AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Event( + id, &AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Events::OnGridSnappingChanged, snapping); + }); + + m_editorViewportSettingsCallbacks->SetGridSnappingChangedEvent(m_gridSnappingHandler); } void EditorViewportWidget::ConnectViewportInteractionRequestBus() @@ -3027,4 +3084,116 @@ float EditorViewportSettings::AngleStep() const return SandboxEditor::AngleSnappingSize(); } +AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once); + +bool EditorViewportWidget::ShouldPreviewFullscreen() const +{ + CLayoutWnd* layout = GetIEditor()->GetViewManager()->GetLayout(); + if (!layout) + { + AZ_Assert(false, "CRenderViewport: No View Manager layout"); + return false; + } + + // Doesn't work with split layout + if (layout->GetLayout() != EViewLayout::ET_Layout0) + { + return false; + } + + // Not supported in VR + if (gSettings.bEnableGameModeVR) + { + return false; + } + + // If level not loaded, don't preview in fullscreen (preview shouldn't work at all without a level, but it does) + if (auto ge = GetIEditor()->GetGameEngine()) + { + if (!ge->IsLevelLoaded()) + { + return false; + } + } + + // Check 'ed_previewGameInFullscreen_once' + if (ed_previewGameInFullscreen_once) + { + ed_previewGameInFullscreen_once = true; + return true; + } + else + { + return false; + } +} + +void EditorViewportWidget::StartFullscreenPreview() +{ + AZ_Assert(!m_inFullscreenPreview, "EditorViewportWidget::StartFullscreenPreview called when already in full screen preview"); + m_inFullscreenPreview = true; + + // Pick the screen on which the main window lies to use as the screen for the full screen preview + const QScreen* screen = MainWindow::instance()->screen(); + const QRect screenGeometry = screen->geometry(); + + // Unparent this and show it, which turns it into a free floating window + // Also set style to frameless and disable resizing by user + setParent(nullptr); + setWindowFlag(Qt::FramelessWindowHint, true); + setWindowFlag(Qt::MSWindowsFixedSizeDialogHint, true); + setFixedSize(screenGeometry.size()); + move(QPoint(screenGeometry.x(), screenGeometry.y())); + showMaximized(); + + // This must be done after unparenting this widget above + MainWindow::instance()->hide(); +} + +void EditorViewportWidget::StopFullscreenPreview() +{ + AZ_Assert(m_inFullscreenPreview, "EditorViewportWidget::StartFullscreenPreview called when not in full screen preview"); + m_inFullscreenPreview = false; + + // Unset frameless window flags + setWindowFlag(Qt::FramelessWindowHint, false); + setWindowFlag(Qt::MSWindowsFixedSizeDialogHint, false); + + // Unset fixed size (note that 50x50 is the minimum set in the constructor) + setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); + setMinimumSize(50, 50); + + // Attach this viewport to the primary view pane (whose index is 0). + if (CLayoutWnd* layout = GetIEditor()->GetViewManager()->GetLayout()) + { + if (CLayoutViewPane* viewPane = layout->GetViewPaneByIndex(0)) + { + // Force-reattach this viewport to its view pane by first detaching + viewPane->DetachViewport(); + viewPane->AttachViewport(this); + + // Set the main widget of the layout, which causes this widgets size to be bound to the layout + // and the viewport title bar to be displayed + layout->SetMainWidget(viewPane); + } + else + { + AZ_Assert(false, "CRenderViewport: No view pane with ID 0 (primary view pane)"); + } + } + else + { + AZ_Assert(false, "CRenderViewport: No View Manager layout"); + } + + // Set this as the selected viewport + GetIEditor()->GetViewManager()->SelectViewport(this); + + // Show this widget (setting flags may hide it) + showNormal(); + + // Show the main window + MainWindow::instance()->show(); +} + #include diff --git a/Code/Sandbox/Editor/EditorViewportWidget.h b/Code/Sandbox/Editor/EditorViewportWidget.h index 6b2db65847..670694f91f 100644 --- a/Code/Sandbox/Editor/EditorViewportWidget.h +++ b/Code/Sandbox/Editor/EditorViewportWidget.h @@ -24,6 +24,7 @@ #include "Objects/DisplayContext.h" #include "Undo/Undo.h" #include "Util/PredefinedAspectRatios.h" +#include "EditorViewportSettings.h" #include #include @@ -385,6 +386,11 @@ protected: }; void ResetToViewSourceType(const ViewSourceType& viewSourType); + bool ShouldPreviewFullscreen() const; + void StartFullscreenPreview(); + void StopFullscreenPreview(); + + bool m_inFullscreenPreview = false; bool m_bRenderContextCreated = false; bool m_bInRotateMode = false; bool m_bInMoveMode = false; @@ -566,6 +572,9 @@ private: AzFramework::EntityVisibilityQuery m_entityVisibilityQuery; + SandboxEditor::GridSnappingChangedEvent::Handler m_gridSnappingHandler; + AZStd::unique_ptr m_editorViewportSettingsCallbacks; + QSet m_keyDown; bool m_freezeViewportInput = false; diff --git a/Code/Sandbox/Editor/LayoutWnd.h b/Code/Sandbox/Editor/LayoutWnd.h index 2af56f907c..66c9a9e889 100644 --- a/Code/Sandbox/Editor/LayoutWnd.h +++ b/Code/Sandbox/Editor/LayoutWnd.h @@ -113,6 +113,8 @@ public: //! Switch 2D viewports. void Cycle2DViewport(); + using AzQtComponents::ToolBarArea::SetMainWidget; + public slots: void ResetLayout(); diff --git a/Code/Sandbox/Editor/MainWindow.cpp b/Code/Sandbox/Editor/MainWindow.cpp index ddc8ff5058..1ad97f31de 100644 --- a/Code/Sandbox/Editor/MainWindow.cpp +++ b/Code/Sandbox/Editor/MainWindow.cpp @@ -949,6 +949,12 @@ void MainWindow::InitActions() .SetApplyHoverEffect() .SetCheckable(true) .RegisterUpdateCallback(cryEdit, &CCryEditApp::OnUpdatePlayGame); + am->AddAction(ID_VIEW_SWITCHTOGAME_FULLSCREEN, tr("Play &Game (Maximized)")) + .SetShortcut(tr("Ctrl+Shift+G")) + .SetStatusTip(tr("Activate the game input mode (maximized)")) + .SetIcon(Style::icon("Play")) + .SetApplyHoverEffect() + .SetCheckable(true); am->AddAction(ID_TOOLBAR_WIDGET_PLAYCONSOLE_LABEL, tr("Play Controls")) .SetText(tr("Play Controls")); am->AddAction(ID_SWITCH_PHYSICS, tr("Simulate")) @@ -1266,10 +1272,25 @@ void MainWindow::OnGameModeChanged(bool inGameMode) { menuBar()->setDisabled(inGameMode); m_toolbarManager->SetEnabled(!inGameMode); - QAction* action = m_actionManager->GetAction(ID_VIEW_SWITCHTOGAME); - action->blockSignals(true); // avoid a loop - action->setChecked(inGameMode); - action->blockSignals(false); + + // block signals on the switch to game actions before setting the checked state, as + // setting the checked state triggers the action, which will re-enter this function + // and result in an infinite loop + AZStd::vector actions = { m_actionManager->GetAction(ID_VIEW_SWITCHTOGAME), m_actionManager->GetAction(ID_VIEW_SWITCHTOGAME_FULLSCREEN) }; + for (auto action : actions) + { + action->blockSignals(true); + } + + for (auto action : actions) + { + action->setChecked(inGameMode); + } + + for (auto action : actions) + { + action->blockSignals(false); + } } void MainWindow::OnEditorNotifyEvent(EEditorNotifyEvent ev) diff --git a/Code/Sandbox/Editor/Resource.h b/Code/Sandbox/Editor/Resource.h index bd6ae94184..8ef6c0b906 100644 --- a/Code/Sandbox/Editor/Resource.h +++ b/Code/Sandbox/Editor/Resource.h @@ -111,6 +111,7 @@ #define ID_EDIT_FETCH 33465 #define ID_FILE_EXPORTTOGAMENOSURFACETEXTURE 33473 #define ID_VIEW_SWITCHTOGAME 33477 +#define ID_VIEW_SWITCHTOGAME_FULLSCREEN 33478 #define ID_EDIT_DELETE 33480 #define ID_MOVE_OBJECT 33481 #define ID_RENAME_OBJ 33483 diff --git a/Code/Sandbox/Editor/Settings.cpp b/Code/Sandbox/Editor/Settings.cpp index 15d3e793a5..f805e2e84b 100644 --- a/Code/Sandbox/Editor/Settings.cpp +++ b/Code/Sandbox/Editor/Settings.cpp @@ -26,6 +26,7 @@ #include #include #include +#include // AzFramework #include @@ -926,6 +927,8 @@ void SEditorSettings::Load() } ////////////////////////////////////////////////////////////////////////// +AZ_CVAR(bool, ed_previewGameInFullscreen_once, false, nullptr, AZ::ConsoleFunctorFlags::IsInvisible, "Preview the game (Ctrl+G, \"Play Game\", etc.) in fullscreen once"); + void SEditorSettings::PostInitApply() { if (!gEnv || !gEnv->pConsole) diff --git a/Code/Sandbox/Editor/ToolbarManager.cpp b/Code/Sandbox/Editor/ToolbarManager.cpp index 137057a4fa..0c68797b6a 100644 --- a/Code/Sandbox/Editor/ToolbarManager.cpp +++ b/Code/Sandbox/Editor/ToolbarManager.cpp @@ -603,6 +603,7 @@ AmazonToolbar ToolbarManager::GetPlayConsoleToolbar() const t.AddAction(ID_TOOLBAR_SEPARATOR, ORIGINAL_TOOLBAR_VERSION); t.AddAction(ID_TOOLBAR_WIDGET_PLAYCONSOLE_LABEL, ORIGINAL_TOOLBAR_VERSION); t.AddAction(ID_VIEW_SWITCHTOGAME, TOOLBARS_WITH_PLAY_GAME); + t.AddAction(ID_VIEW_SWITCHTOGAME_FULLSCREEN, TOOLBARS_WITH_PLAY_GAME); t.AddAction(ID_TOOLBAR_SEPARATOR, ORIGINAL_TOOLBAR_VERSION); t.AddAction(ID_SWITCH_PHYSICS, TOOLBARS_WITH_PLAY_GAME); return t; diff --git a/Code/Sandbox/Editor/ViewportTitleDlg.cpp b/Code/Sandbox/Editor/ViewportTitleDlg.cpp index 48075597cd..2c4b29773d 100644 --- a/Code/Sandbox/Editor/ViewportTitleDlg.cpp +++ b/Code/Sandbox/Editor/ViewportTitleDlg.cpp @@ -181,7 +181,8 @@ void CViewportTitleDlg::SetupCameraDropdownMenu() auto comboBoxTextChanged = static_cast(&QComboBox::currentTextChanged); SetSpeedComboBox(cameraMoveSpeed); - m_cameraSpeed->setInsertPolicy(QComboBox::NoInsert); + m_cameraSpeed->setInsertPolicy(QComboBox::InsertAtBottom); + m_cameraSpeed->setDuplicatesEnabled(false); connect(m_cameraSpeed, comboBoxTextChanged, this, &CViewportTitleDlg::OnUpdateMoveSpeedText); connect(m_cameraSpeed->lineEdit(), &QLineEdit::returnPressed, this, &CViewportTitleDlg::OnSpeedComboBoxEnter); diff --git a/Code/Sandbox/Editor/ViewportTitleDlg.h b/Code/Sandbox/Editor/ViewportTitleDlg.h index 255354dcbb..7978277ce0 100644 --- a/Code/Sandbox/Editor/ViewportTitleDlg.h +++ b/Code/Sandbox/Editor/ViewportTitleDlg.h @@ -117,11 +117,11 @@ protected: // Speed combobox/lineEdit settings double m_minSpeed = 0.01; double m_maxSpeed = 100.0; - double m_speedStep = 0.01; + double m_speedStep = 0.001; int m_numDecimals = 3; // Speed presets - float m_speedPresetValues[3] = { 0.1f, 1.0f, 10.0f }; + float m_speedPresetValues[4] = { 0.01f, 0.1f, 1.0f, 10.0f }; double m_fieldWidthMultiplier = 1.8; diff --git a/Code/Tools/AssetProcessor/assetprocessor_static_files.cmake b/Code/Tools/AssetProcessor/assetprocessor_static_files.cmake index 194e99b247..1e78d1dc6c 100644 --- a/Code/Tools/AssetProcessor/assetprocessor_static_files.cmake +++ b/Code/Tools/AssetProcessor/assetprocessor_static_files.cmake @@ -19,6 +19,7 @@ set(FILES native/AssetManager/AssetRequestHandler.cpp native/AssetManager/AssetRequestHandler.h native/AssetManager/assetScanFolderInfo.h + native/AssetManager/assetScanFolderInfo.cpp native/AssetManager/assetScanner.cpp native/AssetManager/assetScanner.h native/AssetManager/assetScannerWorker.cpp diff --git a/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.cpp b/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.cpp new file mode 100644 index 0000000000..f849b4ee49 --- /dev/null +++ b/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.cpp @@ -0,0 +1,42 @@ +/* +* 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 + +namespace AssetProcessor +{ + ScanFolderInfo::ScanFolderInfo( + QString path, + QString displayName, + QString portableKey, + bool isRoot, + bool recurseSubFolders, + AZStd::vector platforms, + int order, + AZ::s64 scanFolderID, + bool canSaveNewAssets) + : m_scanPath(path) + , m_displayName(displayName) + , m_portableKey (portableKey) + , m_isRoot(isRoot) + , m_recurseSubFolders(recurseSubFolders) + , m_order(order) + , m_scanFolderID(scanFolderID) + , m_platforms(platforms) + , m_canSaveNewAssets(canSaveNewAssets) + { + m_scanPath = AssetUtilities::NormalizeFilePath(m_scanPath); + // note that m_scanFolderID is 0 unless its filled in from the DB. + } + +} // end namespace AssetProcessor diff --git a/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.h b/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.h index 5767822451..5fbda237d7 100644 --- a/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.h +++ b/Code/Tools/AssetProcessor/native/AssetManager/assetScanFolderInfo.h @@ -33,19 +33,7 @@ namespace AssetProcessor AZStd::vector platforms = AZStd::vector{}, int order = 0, AZ::s64 scanFolderID = 0, - bool canSaveNewAssets = false) - : m_scanPath(path) - , m_displayName(displayName) - , m_portableKey (portableKey) - , m_isRoot(isRoot) - , m_recurseSubFolders(recurseSubFolders) - , m_order(order) - , m_scanFolderID(scanFolderID) - , m_platforms(platforms) - , m_canSaveNewAssets(canSaveNewAssets) - { - // note that m_scanFolderID is 0 unless its filled in from the DB. - } + bool canSaveNewAssets = false); ScanFolderInfo() = default; ScanFolderInfo(const ScanFolderInfo& other) = default; diff --git a/Code/Tools/AzTestRunner/CMakeLists.txt b/Code/Tools/AzTestRunner/CMakeLists.txt index e6dd09e15b..fcff173b01 100644 --- a/Code/Tools/AzTestRunner/CMakeLists.txt +++ b/Code/Tools/AzTestRunner/CMakeLists.txt @@ -13,7 +13,7 @@ ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${P include(${pal_dir}/platform_traits_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) -if(PAL_TRAIT_AZTESTRUNNER_SUPPORTED) +if(PAL_TRAIT_AZTESTRUNNER_SUPPORTED AND NOT LY_MONOLITHIC_GAME) ly_add_target( NAME AzTestRunner ${PAL_TRAIT_AZTESTRUNNER_LAUNCHER_TYPE} diff --git a/Code/Tools/ProjectManager/CMakeLists.txt b/Code/Tools/ProjectManager/CMakeLists.txt index aeb7be9793..434ce1424e 100644 --- a/Code/Tools/ProjectManager/CMakeLists.txt +++ b/Code/Tools/ProjectManager/CMakeLists.txt @@ -20,12 +20,11 @@ if (NOT python_package_name) message(WARNING "Python was not found in the package assocation list. Did someone call ly_associate_package(xxxxxxx Python) ?") endif() + ly_add_target( - NAME ProjectManager APPLICATION - OUTPUT_NAME o3de + NAME ProjectManager.Static STATIC NAMESPACE AZ AUTOMOC - AUTORCC FILES_CMAKE project_manager_files.cmake Platform/${PAL_PLATFORM_NAME}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake @@ -47,6 +46,60 @@ ly_add_target( 3rdParty::pybind11 AZ::AzCore AZ::AzFramework - AZ::AzToolsFramework AZ::AzQtComponents -) \ No newline at end of file +) + +ly_add_target( + NAME ProjectManager APPLICATION + OUTPUT_NAME o3de + NAMESPACE AZ + AUTORCC + FILES_CMAKE + project_manager_app_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + BUILD_DEPENDENCIES + PRIVATE + 3rdParty::Qt::Core + 3rdParty::Qt::Concurrent + 3rdParty::Qt::Widgets + 3rdParty::Python + 3rdParty::pybind11 + AZ::AzCore + AZ::AzFramework + AZ::AzQtComponents + AZ::ProjectManager.Static +) + +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME ProjectManager.Tests EXECUTABLE + NAMESPACE AZ + AUTORCC + FILES_CMAKE + project_manager_tests_files.cmake + Platform/${PAL_PLATFORM_NAME}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + Platform/${PAL_PLATFORM_NAME} + BUILD_DEPENDENCIES + PRIVATE + 3rdParty::Qt::Core + 3rdParty::Qt::Concurrent + 3rdParty::Qt::Widgets + 3rdParty::Python + 3rdParty::pybind11 + AZ::AzTest + AZ::AzFramework + AZ::AzFrameworkTestShared + AZ::ProjectManager.Static + ) + + ly_add_googletest( + NAME AZ::ProjectManager.Tests + TEST_COMMAND $ --unittest + ) + +endif() diff --git a/Code/Tools/ProjectManager/Platform/Linux/PAL_linux_tests_files.cmake b/Code/Tools/ProjectManager/Platform/Linux/PAL_linux_tests_files.cmake new file mode 100644 index 0000000000..e79da7183d --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Linux/PAL_linux_tests_files.cmake @@ -0,0 +1,15 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + ProjectManager_Test_Traits_Platform.h + ProjectManager_Test_Traits_Linux.h +) diff --git a/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Linux.h b/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Linux.h new file mode 100644 index 0000000000..c8b428a1c2 --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Linux.h @@ -0,0 +1,15 @@ +/* + * 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 + +#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS true diff --git a/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Platform.h b/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Platform.h new file mode 100644 index 0000000000..639ef8a387 --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Linux/ProjectManager_Test_Traits_Platform.h @@ -0,0 +1,15 @@ +/* + * 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 diff --git a/Code/Tools/ProjectManager/Platform/Mac/PAL_mac_tests_files.cmake b/Code/Tools/ProjectManager/Platform/Mac/PAL_mac_tests_files.cmake new file mode 100644 index 0000000000..a2d480de40 --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Mac/PAL_mac_tests_files.cmake @@ -0,0 +1,15 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + ProjectManager_Test_Traits_Platform.h + ProjectManager_Test_Traits_Mac.h +) diff --git a/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Mac.h b/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Mac.h new file mode 100644 index 0000000000..053db745ea --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Mac.h @@ -0,0 +1,15 @@ +/* + * 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 + +#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS false diff --git a/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Platform.h b/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Platform.h new file mode 100644 index 0000000000..af8817998f --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Mac/ProjectManager_Test_Traits_Platform.h @@ -0,0 +1,15 @@ +/* + * 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 diff --git a/Code/Tools/ProjectManager/Platform/Windows/PAL_windows_tests_files.cmake b/Code/Tools/ProjectManager/Platform/Windows/PAL_windows_tests_files.cmake new file mode 100644 index 0000000000..00d9da3db3 --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Windows/PAL_windows_tests_files.cmake @@ -0,0 +1,15 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES + ProjectManager_Test_Traits_Platform.h + ProjectManager_Test_Traits_Windows.h +) diff --git a/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Platform.h b/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Platform.h new file mode 100644 index 0000000000..915a86644a --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Platform.h @@ -0,0 +1,15 @@ +/* + * 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 diff --git a/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Windows.h b/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Windows.h new file mode 100644 index 0000000000..053db745ea --- /dev/null +++ b/Code/Tools/ProjectManager/Platform/Windows/ProjectManager_Test_Traits_Windows.h @@ -0,0 +1,15 @@ +/* + * 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 + +#define AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS false diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 80470591a8..efc01802b7 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -7,6 +7,11 @@ QMainWindow { margin:0; } +#ScreensCtrl { + min-width:1200px; + min-height:800px; +} + QPushButton:focus { outline: none; border:1px solid #1e70eb; diff --git a/Code/Tools/ProjectManager/Source/Application.cpp b/Code/Tools/ProjectManager/Source/Application.cpp new file mode 100644 index 0000000000..c566e76ef1 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/Application.cpp @@ -0,0 +1,186 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace O3DE::ProjectManager +{ + Application::~Application() + { + TearDown(); + } + + bool Application::Init(bool interactive) + { + constexpr const char* applicationName { "O3DE" }; + + QApplication::setOrganizationName(applicationName); + QApplication::setOrganizationDomain("o3de.org"); + + QCoreApplication::setApplicationName(applicationName); + QCoreApplication::setApplicationVersion("1.0"); + + // Use the LogComponent for non-dev logging log + RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor()); + + // set the log alias to .o3de/Logs instead of the default user/logs + AZ::IO::FixedMaxPath path = AZ::Utils::GetO3deLogsDirectory(); + + // DevWriteStorage is where the event log is written during development + m_settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_DevWriteStorage, path.LexicallyNormal().Native()); + + // Save event logs to .o3de/Logs/eventlogger/EventLogO3DE.azsl + m_settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::BuildTargetNameKey, applicationName); + + Start(AzFramework::Application::Descriptor()); + + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); + + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware); + + // Create the actual Qt Application - this needs to happen before using QMessageBox + m_app.reset(new QApplication(*GetArgC(), *GetArgV())); + + if(!InitLog(applicationName)) + { + AZ_Warning("ProjectManager", false, "Failed to init logging"); + } + + m_pythonBindings = AZStd::make_unique(GetEngineRoot()); + if (!m_pythonBindings || !m_pythonBindings->PythonStarted()) + { + if (interactive) + { + QMessageBox::critical(nullptr, QObject::tr("Failed to start Python"), + QObject::tr("This tool requires an O3DE engine with a Python runtime, " + "but either Python is missing or mis-configured. Please rename " + "your python/runtime folder to python/runtime_bak, then run " + "python/get_python.bat to restore the Python runtime folder.")); + } + return false; + } + + const AZ::CommandLine* commandLine = GetCommandLine(); + AZ_Assert(commandLine, "Failed to get command line"); + + ProjectManagerScreen startScreen = ProjectManagerScreen::Projects; + if (size_t screenSwitchCount = commandLine->GetNumSwitchValues("screen"); screenSwitchCount > 0) + { + QString screenOption = commandLine->GetSwitchValue("screen", screenSwitchCount - 1).c_str(); + ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption); + if (screen != ProjectManagerScreen::Invalid) + { + startScreen = screen; + } + } + + AZ::IO::FixedMaxPath projectPath; + if (size_t projectSwitchCount = commandLine->GetNumSwitchValues("project-path"); projectSwitchCount > 0) + { + projectPath = commandLine->GetSwitchValue("project-path", projectSwitchCount - 1).c_str(); + } + + m_mainWindow.reset(new ProjectManagerWindow(nullptr, projectPath, startScreen)); + + return true; + } + + bool Application::InitLog(const char* logName) + { + if (!m_entity) + { + // override the log alias to the O3de Logs directory instead of the default project user/Logs folder + AZ::IO::FixedMaxPath path = AZ::Utils::GetO3deLogsDirectory(); + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + AZ_Assert(fileIO, "Failed to get FileIOBase instance"); + + fileIO->SetAlias("@log@", path.LexicallyNormal().Native().c_str()); + + // this entity exists because we need a home for LogComponent + // and cannot use the system entity because we need to be able to call SetLogFileBaseName + // so the log will be named O3DE.log + m_entity = aznew AZ::Entity("Application Entity"); + if (m_entity) + { + AzFramework::LogComponent* logger = aznew AzFramework::LogComponent(); + AZ_Assert(logger, "Failed to create LogComponent"); + logger->SetLogFileBaseName(logName); + m_entity->AddComponent(logger); + m_entity->Init(); + m_entity->Activate(); + } + } + + return m_entity != nullptr; + } + + void Application::TearDown() + { + if (m_entity) + { + m_entity->Deactivate(); + delete m_entity; + m_entity = nullptr; + } + + m_pythonBindings.reset(); + m_mainWindow.reset(); + m_app.reset(); + } + + bool Application::Run() + { + // Set up the Style Manager + AzQtComponents::StyleManager styleManager(qApp); + styleManager.initialize(qApp, GetEngineRoot()); + + // setup stylesheets and hot reloading + AZ::IO::FixedMaxPath engineRoot(GetEngineRoot()); + QDir rootDir(engineRoot.c_str()); + const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources"); + const auto qrcPath = QStringLiteral(":/ProjectManager/style"); + AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRoot); + + // set stylesheet after creating the main window or their styles won't get updated + AzQtComponents::StyleManager::setStyleSheet(m_mainWindow.data(), QStringLiteral("style:ProjectManager.qss")); + + // the decoration wrapper is intended to remember window positioning and sizing + auto wrapper = new AzQtComponents::WindowDecorationWrapper(); + wrapper->setGuest(m_mainWindow.data()); + wrapper->show(); + m_mainWindow->show(); + + qApp->setQuitOnLastWindowClosed(true); + + // Run the application + return qApp->exec(); + } + +} diff --git a/Code/Tools/ProjectManager/Source/Application.h b/Code/Tools/ProjectManager/Source/Application.h new file mode 100644 index 0000000000..d4e94e8dd4 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/Application.h @@ -0,0 +1,48 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#include +#include +#endif + +namespace AZ +{ + class Entity; +} + +namespace O3DE::ProjectManager +{ + class Application + : public AzFramework::Application + { + public: + using AzFramework::Application::Application; + virtual ~Application(); + + bool Init(bool interactive = true); + bool Run(); + void TearDown(); + + private: + bool InitLog(const char* logName); + + AZStd::unique_ptr m_pythonBindings; + QSharedPointer m_app; + QSharedPointer m_mainWindow; + + AZ::Entity* m_entity = nullptr; + }; +} diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp index 6342041da4..cf597745ea 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp @@ -25,7 +25,7 @@ namespace O3DE::ProjectManager EngineSettingsScreen::EngineSettingsScreen(QWidget* parent) : ScreenWidget(parent) { - auto* layout = new QVBoxLayout(this); + auto* layout = new QVBoxLayout(); layout->setAlignment(Qt::AlignTop); setObjectName("engineSettingsScreen"); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 4424767c0b..2c92af4e51 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -82,6 +82,8 @@ namespace O3DE::ProjectManager m_headerWidget->ReinitForProject(); + connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); + // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ QModelIndex firstModelIndex = m_gemListView->model()->index(0,0); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp index a6a4e95ff9..7c6150ff99 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp @@ -26,6 +26,7 @@ namespace O3DE::ProjectManager const QVector& elementNames, const QVector& elementCounts, bool showAllLessButton, + bool collapsed, int defaultShowCount, QWidget* parent) : QWidget(parent) @@ -40,6 +41,7 @@ namespace O3DE::ProjectManager QHBoxLayout* collapseLayout = new QHBoxLayout(); m_collapseButton = new QPushButton(); m_collapseButton->setCheckable(true); + m_collapseButton->setChecked(collapsed); m_collapseButton->setFlat(true); m_collapseButton->setFocusPolicy(Qt::NoFocus); m_collapseButton->setFixedWidth(s_collapseButtonSize); @@ -178,6 +180,11 @@ namespace O3DE::ProjectManager return m_buttonGroup; } + bool FilterCategoryWidget::IsCollapsed() + { + return m_collapseButton->isChecked(); + } + GemFilterWidget::GemFilterWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent) : QScrollArea(parent) , m_filterProxyModel(filterProxyModel) @@ -193,20 +200,106 @@ namespace O3DE::ProjectManager QWidget* mainWidget = new QWidget(); setWidget(mainWidget); - m_mainLayout = new QVBoxLayout(); - m_mainLayout->setAlignment(Qt::AlignTop); - mainWidget->setLayout(m_mainLayout); + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->setAlignment(Qt::AlignTop); + mainWidget->setLayout(mainLayout); QLabel* filterByLabel = new QLabel("Filter by"); filterByLabel->setStyleSheet("font-size: 16px;"); - m_mainLayout->addWidget(filterByLabel); + mainLayout->addWidget(filterByLabel); + + QWidget* filterSection = new QWidget(this); + mainLayout->addWidget(filterSection); + + m_filterLayout = new QVBoxLayout(); + m_filterLayout->setAlignment(Qt::AlignTop); + m_filterLayout->setContentsMargins(0, 0, 0, 0); + filterSection->setLayout(m_filterLayout); + ResetGemStatusFilter(); AddGemOriginFilter(); AddTypeFilter(); AddPlatformFilter(); AddFeatureFilter(); } + void GemFilterWidget::ResetGemStatusFilter() + { + QVector elementNames; + QVector elementCounts; + const int totalGems = m_gemModel->rowCount(); + const int selectedGemTotal = m_gemModel->TotalAddedGems(); + + elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Unselected)); + elementCounts.push_back(totalGems - selectedGemTotal); + + elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Selected)); + elementCounts.push_back(selectedGemTotal); + + bool wasCollapsed = false; + if (m_statusFilter) + { + wasCollapsed = m_statusFilter->IsCollapsed(); + } + + FilterCategoryWidget* filterWidget = + new FilterCategoryWidget("Status", elementNames, elementCounts, /*showAllLessButton=*/false, /*collapsed*/wasCollapsed); + if (m_statusFilter) + { + m_filterLayout->replaceWidget(m_statusFilter, filterWidget); + } + else + { + m_filterLayout->addWidget(filterWidget); + } + + m_statusFilter->deleteLater(); + m_statusFilter = filterWidget; + + const GemSortFilterProxyModel::GemStatus currentFilterState = m_filterProxyModel->GetGemStatus(); + const QList buttons = m_statusFilter->GetButtonGroup()->buttons(); + for (int statusFilterIndex = 0; statusFilterIndex < buttons.size(); ++statusFilterIndex) + { + const GemSortFilterProxyModel::GemStatus gemStatus = static_cast(statusFilterIndex); + QAbstractButton* button = buttons[statusFilterIndex]; + + if (static_cast(statusFilterIndex) == currentFilterState) + { + button->setChecked(true); + } + + connect( + button, &QAbstractButton::toggled, this, + [=](bool checked) + { + GemSortFilterProxyModel::GemStatus filterStatus = m_filterProxyModel->GetGemStatus(); + if (checked) + { + if (filterStatus == GemSortFilterProxyModel::GemStatus::NoFilter) + { + filterStatus = gemStatus; + } + else + { + filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter; + } + } + else + { + if (filterStatus != gemStatus) + { + filterStatus = static_cast(!gemStatus); + } + else + { + filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter; + } + } + m_filterProxyModel->SetGemStatus(filterStatus); + }); + } + } + void GemFilterWidget::AddGemOriginFilter() { QVector elementNames; @@ -233,7 +326,7 @@ namespace O3DE::ProjectManager } FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Provider", elementNames, elementCounts, /*showAllLessButton=*/false); - m_mainLayout->addWidget(filterWidget); + m_filterLayout->addWidget(filterWidget); const QList buttons = filterWidget->GetButtonGroup()->buttons(); for (int i = 0; i < buttons.size(); ++i) @@ -283,7 +376,7 @@ namespace O3DE::ProjectManager } FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Type", elementNames, elementCounts, /*showAllLessButton=*/false); - m_mainLayout->addWidget(filterWidget); + m_filterLayout->addWidget(filterWidget); const QList buttons = filterWidget->GetButtonGroup()->buttons(); for (int i = 0; i < buttons.size(); ++i) @@ -333,7 +426,7 @@ namespace O3DE::ProjectManager } FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Supported Platforms", elementNames, elementCounts, /*showAllLessButton=*/false); - m_mainLayout->addWidget(filterWidget); + m_filterLayout->addWidget(filterWidget); const QList buttons = filterWidget->GetButtonGroup()->buttons(); for (int i = 0; i < buttons.size(); ++i) @@ -388,8 +481,8 @@ namespace O3DE::ProjectManager } FilterCategoryWidget* filterWidget = new FilterCategoryWidget("Features", elementNames, elementCounts, - /*showAllLessButton=*/true, /*defaultShowCount=*/5); - m_mainLayout->addWidget(filterWidget); + /*showAllLessButton=*/true, false, /*defaultShowCount=*/5); + m_filterLayout->addWidget(filterWidget); const QList buttons = filterWidget->GetButtonGroup()->buttons(); for (int i = 0; i < buttons.size(); ++i) diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h index 017eadc020..520370eb44 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.h @@ -37,11 +37,14 @@ namespace O3DE::ProjectManager const QVector& elementNames, const QVector& elementCounts, bool showAllLessButton = true, + bool collapsed = false, int defaultShowCount = 4, QWidget* parent = nullptr); QButtonGroup* GetButtonGroup(); + bool IsCollapsed(); + private: void UpdateCollapseState(); void UpdateSeeMoreLess(); @@ -66,14 +69,18 @@ namespace O3DE::ProjectManager explicit GemFilterWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr); ~GemFilterWidget() = default; + public slots: + void ResetGemStatusFilter(); + private: void AddGemOriginFilter(); void AddTypeFilter(); void AddPlatformFilter(); void AddFeatureFilter(); - QVBoxLayout* m_mainLayout = nullptr; + QVBoxLayout* m_filterLayout = nullptr; GemModel* m_gemModel = nullptr; GemSortFilterProxyModel* m_filterProxyModel = nullptr; + FilterCategoryWidget* m_statusFilter = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp index 8aa68fb7a2..03787de7e8 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp @@ -204,7 +204,6 @@ namespace O3DE::ProjectManager painter->save(); const QRect buttonRect = CalcButtonRect(contentRect); QPoint circleCenter; - QString buttonText; const bool isAdded = GemModel::IsAdded(modelIndex); if (isAdded) @@ -213,34 +212,15 @@ namespace O3DE::ProjectManager painter->setPen(m_buttonEnabledColor); circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1); - buttonText = "Added"; } else { circleCenter = buttonRect.center() + QPoint(-buttonRect.width() / 2 + s_buttonBorderRadius, 1); - buttonText = "Get"; } // Rounded rect painter->drawRoundedRect(buttonRect, s_buttonBorderRadius, s_buttonBorderRadius); - // Text - QFont font; - QRect textRect = GetTextRect(font, buttonText, s_buttonFontSize); - if (isAdded) - { - textRect = QRect(buttonRect.left(), buttonRect.top(), buttonRect.width() - s_buttonCircleRadius * 2.0, buttonRect.height()); - } - else - { - textRect = QRect(buttonRect.left() + s_buttonCircleRadius * 2.0, buttonRect.top(), buttonRect.width() - s_buttonCircleRadius * 2.0, buttonRect.height()); - } - - font.setPixelSize(s_buttonFontSize); - painter->setFont(font); - painter->setPen(m_textColor); - painter->drawText(textRect, Qt::AlignCenter, buttonText); - // Circle painter->setBrush(m_textColor); painter->drawEllipse(circleCenter, s_buttonCircleRadius, s_buttonCircleRadius); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp index bc287e3c61..ad1b57a27c 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -74,6 +75,15 @@ namespace O3DE::ProjectManager gemSummaryLabel->setStyleSheet("font-size: 12px;"); columnHeaderLayout->addWidget(gemSummaryLabel); + QSpacerItem* horizontalSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + columnHeaderLayout->addSpacerItem(horizontalSpacer); + + QLabel* gemSelectedLabel = new QLabel(tr("Selected")); + gemSelectedLabel->setStyleSheet("font-size: 12px;"); + columnHeaderLayout->addWidget(gemSelectedLabel); + + columnHeaderLayout->addSpacing(60); + vLayout->addLayout(columnHeaderLayout); } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 6c09c95572..5dc40723c9 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -235,4 +235,19 @@ namespace O3DE::ProjectManager } return result; } + + int GemModel::TotalAddedGems() const + { + int result = 0; + for (int row = 0; row < rowCount(); ++row) + { + const QModelIndex modelIndex = index(row, 0); + if (IsAdded(modelIndex)) + { + ++result; + } + } + return result; + } + } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 77f973a91c..2e05472cdf 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -63,6 +63,8 @@ namespace O3DE::ProjectManager QVector GatherGemsToBeAdded() const; QVector GatherGemsToBeRemoved() const; + int TotalAddedGems() const; + private: enum UserRole { diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp index d8f41c077e..c1360cabc6 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp @@ -37,6 +37,16 @@ namespace O3DE::ProjectManager return false; } + // Gem status + if (m_gemStatusFilter != GemStatus::NoFilter) + { + const GemStatus sourceGemStatus = static_cast(GemModel::IsAdded(sourceIndex)); + if (m_gemStatusFilter != sourceGemStatus) + { + return false; + } + } + // Gem origins if (m_gemOriginFilter) { @@ -125,6 +135,19 @@ namespace O3DE::ProjectManager return true; } + QString GemSortFilterProxyModel::GetGemStatusString(GemStatus status) + { + switch (status) + { + case Unselected: + return "Unselected"; + case Selected: + return "Selected"; + default: + return ""; + } + } + void GemSortFilterProxyModel::InvalidateFilter() { invalidate(); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h index f24a724ecf..fcde226f40 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h @@ -29,8 +29,17 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: + enum GemStatus + { + NoFilter = -1, + Unselected, + Selected + }; + GemSortFilterProxyModel(GemModel* sourceModel, QObject* parent = nullptr); + static QString GetGemStatusString(GemStatus status); + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; GemModel* GetSourceModel() const { return m_sourceModel; } @@ -38,6 +47,9 @@ namespace O3DE::ProjectManager void SetSearchString(const QString& searchString) { m_searchString = searchString; InvalidateFilter(); } + GemStatus GetGemStatus() const { return m_gemStatusFilter; } + void SetGemStatus(GemStatus gemStatus) { m_gemStatusFilter = gemStatus; InvalidateFilter(); } + GemInfo::GemOrigins GetGemOrigins() const { return m_gemOriginFilter; } void SetGemOrigins(const GemInfo::GemOrigins& gemOrigins) { m_gemOriginFilter = gemOrigins; InvalidateFilter(); } @@ -61,6 +73,7 @@ namespace O3DE::ProjectManager AzQtComponents::SelectionProxyModel* m_selectionProxyModel = nullptr; QString m_searchString; + GemStatus m_gemStatusFilter = GemStatus::NoFilter; GemInfo::GemOrigins m_gemOriginFilter = {}; GemInfo::Platforms m_platformFilter = {}; GemInfo::Types m_typeFilter = {}; diff --git a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp index 761572ffa5..3bde0a310d 100644 --- a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp @@ -33,7 +33,7 @@ namespace O3DE::ProjectManager { setObjectName("labelButton"); - QVBoxLayout* vLayout = new QVBoxLayout(this); + QVBoxLayout* vLayout = new QVBoxLayout(); vLayout->setContentsMargins(0, 0, 0, 0); vLayout->setSpacing(5); diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index cb1398cc61..59be0aaa35 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -13,21 +13,11 @@ #include #include -#include -#include -#include -#include -#include - -#include - namespace O3DE::ProjectManager { - ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen) + ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen) : QMainWindow(parent) { - m_pythonBindings = AZStd::make_unique(engineRootPath); - setWindowTitle(tr("O3DE Project Manager")); ScreensCtrl* screensCtrl = new ScreensCtrl(); @@ -44,15 +34,6 @@ namespace O3DE::ProjectManager setCentralWidget(screensCtrl); - // setup stylesheets and hot reloading - QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast(engineRootPath.Native().size())); - const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources"); - const auto qrcPath = QStringLiteral(":/ProjectManager/style"); - AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRootPath); - - // set stylesheet after creating the screens or their styles won't get updated - AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss")); - // always push the projects screen first so we have something to come back to if (startScreen != ProjectManagerScreen::Projects) { @@ -66,10 +47,4 @@ namespace O3DE::ProjectManager emit screensCtrl->NotifyCurrentProject(path); } } - - ProjectManagerWindow::~ProjectManagerWindow() - { - m_pythonBindings.reset(); - } - } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h index 758af8fc00..db2b1fd304 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h @@ -13,7 +13,7 @@ #if !defined(Q_MOC_RUN) #include -#include +#include #include #endif @@ -25,12 +25,8 @@ namespace O3DE::ProjectManager Q_OBJECT public: - explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, + explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen = ProjectManagerScreen::Projects); - ~ProjectManagerWindow(); - - private: - AZStd::unique_ptr m_pythonBindings; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp index 26711753d4..b198724353 100644 --- a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp @@ -37,7 +37,7 @@ namespace O3DE::ProjectManager // if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally QFrame* projectSettingsFrame = new QFrame(this); projectSettingsFrame->setObjectName("projectSettings"); - m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout = new QVBoxLayout(); // you cannot remove content margins in qss m_verticalLayout->setContentsMargins(0, 0, 0, 0); diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp index 8e41e52643..6633558406 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp @@ -85,7 +85,7 @@ namespace O3DE::ProjectManager QFrame* frame = new QFrame(this); frame->setObjectName("firstTimeContent"); { - QVBoxLayout* layout = new QVBoxLayout(this); + QVBoxLayout* layout = new QVBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->setAlignment(Qt::AlignTop); frame->setLayout(layout); @@ -100,7 +100,7 @@ namespace O3DE::ProjectManager "available by downloading our sample project.")); layout->addWidget(introLabel); - QHBoxLayout* buttonLayout = new QHBoxLayout(this); + QHBoxLayout* buttonLayout = new QHBoxLayout(); buttonLayout->setAlignment(Qt::AlignLeft); buttonLayout->setSpacing(s_spacerSize); diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index bb6c05a472..0e00319b6b 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -226,7 +226,7 @@ namespace O3DE::ProjectManager PythonBindings::PythonBindings(const AZ::IO::PathView& enginePath) : m_enginePath(enginePath) { - StartPython(); + m_pythonStarted = StartPython(); } PythonBindings::~PythonBindings() @@ -234,6 +234,11 @@ namespace O3DE::ProjectManager StopPython(); } + bool PythonBindings::PythonStarted() + { + return m_pythonStarted && Py_IsInitialized(); + } + bool PythonBindings::StartPython() { if (Py_IsInitialized()) @@ -246,7 +251,7 @@ namespace O3DE::ProjectManager AZStd::string pyBasePath = Platform::GetPythonHomePath(PY_PACKAGE, m_enginePath.c_str()); if (!AZ::IO::SystemFile::Exists(pyBasePath.c_str())) { - AZ_Assert(false, "Python home path must exist. path:%s", pyBasePath.c_str()); + AZ_Error("python", false, "Python home path does not exist: %s", pyBasePath.c_str()); return false; } @@ -351,6 +356,11 @@ namespace O3DE::ProjectManager AZ::Outcome PythonBindings::ExecuteWithLockErrorHandling(AZStd::function executionCallback) { + if (!Py_IsInitialized()) + { + return AZ::Failure("Python is not initialized"); + } + AZStd::lock_guard lock(m_lock); pybind11::gil_scoped_release release; pybind11::gil_scoped_acquire acquire; diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 5700ede850..065867a130 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -34,6 +34,8 @@ namespace O3DE::ProjectManager ~PythonBindings() override; // PythonBindings overrides + bool PythonStarted() override; + // Engine AZ::Outcome GetEngineInfo() override; bool SetEngineInfo(const EngineInfo& engineInfo) override; @@ -70,6 +72,8 @@ namespace O3DE::ProjectManager bool StopPython(); + bool m_pythonStarted = false; + AZ::IO::FixedMaxPath m_enginePath; pybind11::handle m_engineTemplate; AZStd::recursive_mutex m_lock; diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index bc20d8e3f0..6d72bfee0c 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -34,6 +34,12 @@ namespace O3DE::ProjectManager IPythonBindings() = default; virtual ~IPythonBindings() = default; + /** + * Get whether Python was started or not. All Python functionality will fail if Python + * failed to start. + * @return true if Python was started successfully, false on failure + */ + virtual bool PythonStarted() = 0; // Engine diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp index be1f0e5529..65f73accd1 100644 --- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp @@ -189,11 +189,13 @@ namespace O3DE::ProjectManager { if (m_stack->currentIndex() == ScreenOrder::Gems) { - m_header->setSubTitle(QString(tr("Configure Gems for \"%1\"")).arg(m_projectInfo.m_projectName)); - m_nextButton->setText(tr("Confirm")); + m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName)); + m_header->setSubTitle(QString(tr("Configure Gems"))); + m_nextButton->setText(tr("Finalize")); } else { + m_header->setTitle(""); m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName)); m_nextButton->setText(tr("Save")); } diff --git a/Code/Tools/ProjectManager/Source/main.cpp b/Code/Tools/ProjectManager/Source/main.cpp index c597b8a729..1f4cadfb14 100644 --- a/Code/Tools/ProjectManager/Source/main.cpp +++ b/Code/Tools/ProjectManager/Source/main.cpp @@ -10,85 +10,26 @@ * */ -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include - -using namespace O3DE::ProjectManager; +#include +#include int main(int argc, char* argv[]) { - QApplication::setOrganizationName("O3DE"); - QApplication::setOrganizationDomain("o3de.org"); - QCoreApplication::setApplicationName("ProjectManager"); - QCoreApplication::setApplicationVersion("1.0"); - - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); - AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware); - - AZ::AllocatorInstance::Create(); int runSuccess = 0; - { - QApplication app(argc, argv); - - // Need to use settings registry to get EngineRootFolder - AZ::IO::FixedMaxPath engineRootPath; - { - AZ::ComponentApplication componentApplication; - auto settingsRegistry = AZ::SettingsRegistry::Get(); - settingsRegistry->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder); - } - - AzQtComponents::StyleManager styleManager(&app); - styleManager.initialize(&app, engineRootPath); - // Get the initial start screen if one is provided via command line - constexpr char optionPrefix[] = "--"; - AZ::CommandLine commandLine(optionPrefix); - commandLine.Parse(argc, argv); + // Call before using any Qt, or the app may not be able to locate Qt libs + AzQtComponents::PrepareQtPaths(); - ProjectManagerScreen startScreen = ProjectManagerScreen::Projects; - if(commandLine.HasSwitch("screen")) - { - QString screenOption = commandLine.GetSwitchValue("screen", 0).c_str(); - ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption); - if (screen != ProjectManagerScreen::Invalid) - { - startScreen = screen; - } - } - - AZ::IO::FixedMaxPath projectPath; - if (commandLine.HasSwitch("project-path")) - { - projectPath = commandLine.GetSwitchValue("project-path", 0).c_str(); - } - - ProjectManagerWindow window(nullptr, engineRootPath, projectPath, startScreen); - window.show(); - - // somethings is preventing us from moving the window to the center of the - // primary screen - likely an Az style or component helper - constexpr int width = 1200; - constexpr int height = 800; - window.resize(width, height); - - runSuccess = app.exec(); + O3DE::ProjectManager::Application application(&argc, &argv); + if (!application.Init()) + { + AZ_Error("ProjectManager", false, "Failed to initialize"); + runSuccess = 1; + } + else + { + runSuccess = application.Run() ? 0 : 1; } - AZ::AllocatorInstance::Destroy(); return runSuccess; } diff --git a/Code/Tools/ProjectManager/project_manager_app_files.cmake b/Code/Tools/ProjectManager/project_manager_app_files.cmake new file mode 100644 index 0000000000..223683ddd9 --- /dev/null +++ b/Code/Tools/ProjectManager/project_manager_app_files.cmake @@ -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. +# + +set(FILES + Resources/ProjectManager.rc + Resources/ProjectManager.qrc + Resources/ProjectManager.qss + Source/main.cpp +) diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 633824f995..587b5907bb 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -1,4 +1,5 @@ # +# # All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or # its licensors. # @@ -10,10 +11,8 @@ # set(FILES - Resources/ProjectManager.rc - Resources/ProjectManager.qrc - Resources/ProjectManager.qss - Source/main.cpp + Source/Application.h + Source/Application.cpp Source/ScreenDefs.h Source/ScreenFactory.h Source/ScreenFactory.cpp diff --git a/Code/Tools/ProjectManager/project_manager_tests_files.cmake b/Code/Tools/ProjectManager/project_manager_tests_files.cmake new file mode 100644 index 0000000000..e1e84a43a7 --- /dev/null +++ b/Code/Tools/ProjectManager/project_manager_tests_files.cmake @@ -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. +# + +set(FILES + Resources/ProjectManager.qrc + Resources/ProjectManager.qss + tests/ApplicationTests.cpp + tests/main.cpp +) diff --git a/Code/Tools/ProjectManager/tests/ApplicationTests.cpp b/Code/Tools/ProjectManager/tests/ApplicationTests.cpp new file mode 100644 index 0000000000..c98b1a3a6f --- /dev/null +++ b/Code/Tools/ProjectManager/tests/ApplicationTests.cpp @@ -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. +* +*/ + +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + class ProjectManagerApplicationTests + : public ::UnitTest::ScopedAllocatorSetupFixture + { + public: + + ProjectManagerApplicationTests() + { + m_application = AZStd::make_unique(); + } + + ~ProjectManagerApplicationTests() + { + m_application.reset(); + } + + AZStd::unique_ptr m_application; + }; + +#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS + TEST_F(ProjectManagerApplicationTests, DISABLED_Application_Init_Succeeds) +#else + TEST_F(ProjectManagerApplicationTests, Application_Init_Succeeds) +#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS + { + // we don't want to interact with actual GUI or display it + EXPECT_TRUE(m_application->Init(/*interactive=*/false)); + } +} diff --git a/Code/Tools/ProjectManager/tests/main.cpp b/Code/Tools/ProjectManager/tests/main.cpp new file mode 100644 index 0000000000..191bef846a --- /dev/null +++ b/Code/Tools/ProjectManager/tests/main.cpp @@ -0,0 +1,35 @@ +/* +* 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 + +DECLARE_AZ_UNIT_TEST_MAIN(); + +int runDefaultRunner(int argc, char* argv[]) +{ + INVOKE_AZ_UNIT_TEST_MAIN(nullptr) + return 0; +} + +int main(int argc, char* argv[]) +{ + if (argc == 1) + { + // if no parameters are provided, add the --unittests parameter + constexpr int defaultArgc = 2; + char unittest_arg[] = "--unittests"; // Conversion from string literal to char* is not allowed per ISO C++11 + char* defaultArgv[defaultArgc] = { argv[0], unittest_arg }; + return runDefaultRunner(defaultArgc, defaultArgv); + } + INVOKE_AZ_UNIT_TEST_MAIN(nullptr); + return 0; +} diff --git a/Code/Tools/PythonBindingsExample/CMakeLists.txt b/Code/Tools/PythonBindingsExample/CMakeLists.txt index 3abd24b5fc..90cb718ce4 100644 --- a/Code/Tools/PythonBindingsExample/CMakeLists.txt +++ b/Code/Tools/PythonBindingsExample/CMakeLists.txt @@ -114,6 +114,6 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_add_googletest( NAME AZ::PythonBindingsExample.Tests - TEST_COMMAND $ + TEST_COMMAND $ --unittest ) endif() diff --git a/Code/Tools/PythonBindingsExample/tests/ApplicationTests.cpp b/Code/Tools/PythonBindingsExample/tests/ApplicationTests.cpp index e9ea13fb82..a57b35fb7e 100644 --- a/Code/Tools/PythonBindingsExample/tests/ApplicationTests.cpp +++ b/Code/Tools/PythonBindingsExample/tests/ApplicationTests.cpp @@ -63,9 +63,9 @@ namespace PythonBindingsExample AZStd::unique_ptr PythonBindingsExampleTest::s_application; - TEST_F(PythonBindingsExampleTest, Application_Run_Fails) + TEST_F(PythonBindingsExampleTest, Application_Run_Succeeds) { - EXPECT_FALSE(s_application->Run()); + EXPECT_TRUE(s_application->Run()); } TEST_F(PythonBindingsExampleTest, Application_RunWithParameters_Works) diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp index 5357c32fa9..bcc007e3a7 100644 --- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp +++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp @@ -46,22 +46,69 @@ namespace AZ serializeContext->Class()->Version(1); } } - + + void GetAllBones(const aiScene* scene, AZStd::unordered_map& boneLookup) + { + for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) + { + const aiMesh* mesh = scene->mMeshes[meshIndex]; + + for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex) + { + const aiBone* bone = mesh->mBones[boneIndex]; + + boneLookup[bone->mName.C_Str()] = bone; + } + } + } + Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context) { AZ_TraceContext("Importer", "transform"); const aiNode* currentNode = context.m_sourceNode.GetAssImpNode(); const aiScene* scene = context.m_sourceScene.GetAssImpScene(); - + if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName)) { return Events::ProcessingResult::Ignored; } - aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode); + AZStd::unordered_map boneLookup; + GetAllBones(scene, boneLookup); + + auto boneIterator = boneLookup.find(currentNode->mName.C_Str()); + const bool isBone = boneIterator != boneLookup.end(); + + aiMatrix4x4 combinedTransform; + + if (isBone) + { + auto parentNode = currentNode->mParent; + + aiMatrix4x4 offsetMatrix = boneIterator->second->mOffsetMatrix; + aiMatrix4x4 parentOffset {}; + + auto parentBoneIterator = boneLookup.find(parentNode->mName.C_Str()); + + if (parentNode && parentBoneIterator != boneLookup.end()) + { + const auto& parentBone = parentBoneIterator->second; + + parentOffset = parentBone->mOffsetMatrix; + } + + auto inverseOffset = offsetMatrix; + inverseOffset.Inverse(); + + combinedTransform = parentOffset * inverseOffset; + } + else + { + combinedTransform = GetConcatenatedLocalTransform(currentNode); + } DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform); - + context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform); context.m_sourceSceneSystem.ConvertUnit(localTransform); @@ -105,9 +152,7 @@ namespace AZ } else { - bool addedData = context.m_scene.GetGraph().SetContent( - context.m_currentGraphPosition, - transformData); + bool addedData = context.m_scene.GetGraph().SetContent(context.m_currentGraphPosition, transformData); AZ_Error(SceneAPI::Utilities::ErrorWindow, addedData, "Failed to add node data"); return addedData ? Events::ProcessingResult::Success : Events::ProcessingResult::Failure; diff --git a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp index 336cc1b172..3c01255cb9 100644 --- a/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp +++ b/Code/Tools/SceneAPI/SDKWrapper/AssImpSceneWrapper.cpp @@ -69,13 +69,14 @@ namespace AZ // aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this, // this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release. // There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph. + + // aiProcess_LimitBoneWeights is not enabled because it will remove bones which are not associated with a mesh. + // This results in the loss of the offset matrix data for nodes without a mesh which is required for the Transform Importer. m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false); m_sceneFileName = fileName; m_assImpScene = m_importer.ReadFile(fileName, aiProcess_Triangulate //Triangulates all faces of all meshes - | aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value - //dropping the least important and re-normalizing | aiProcess_GenNormals); //Generate normals for meshes #if AZ_TRAIT_COMPILER_SUPPORT_CSIGNAL diff --git a/Code/Tools/SerializeContextTools/Converter.cpp b/Code/Tools/SerializeContextTools/Converter.cpp index 0b19d4e981..27bd1fc495 100644 --- a/Code/Tools/SerializeContextTools/Converter.cpp +++ b/Code/Tools/SerializeContextTools/Converter.cpp @@ -757,14 +757,21 @@ namespace AZ { using namespace AZ::JsonSerializationResult; + // Need special handling if the original type is `any', because `CreateAny' creates an empty `any' in that case, + // because it's not possible to store an any inside an any + const bool originalTypeIsAny = originalType == azrtti_typeid(); + AZStd::any convertedDeserialized = settings.m_serializeContext->CreateAny(originalType); - if (convertedDeserialized.empty()) + if (!originalTypeIsAny && convertedDeserialized.empty()) { AZ_Printf("Convert", " Failed to deserialized from converted document.\n"); return false; } - ResultCode loadResult = JsonSerialization::Load(AZStd::any_cast(&convertedDeserialized), originalType, convertedData, settings); + // Get a storage suitable to hold this data. + void* objectPtr = originalTypeIsAny ? &convertedDeserialized : AZStd::any_cast(&convertedDeserialized); + + ResultCode loadResult = JsonSerialization::Load(objectPtr, originalType, convertedData, settings); if (loadResult.GetProcessing() == Processing::Halted) { AZ_Printf("Convert", " Failed to verify converted document because it couldn't be loaded.\n"); @@ -782,7 +789,7 @@ namespace AZ bool result = false; if (data->m_serializer) { - result = data->m_serializer->CompareValueData(original, AZStd::any_cast(&convertedDeserialized)); + result = data->m_serializer->CompareValueData(original, objectPtr); } else { @@ -793,7 +800,7 @@ namespace AZ AZStd::vector loadedData; AZ::IO::ByteContainerStream loadedStream(&loadedData); AZ::Utils::SaveObjectToStream(loadedStream, AZ::ObjectStream::ST_BINARY, - AZStd::any_cast(&convertedDeserialized), convertedDeserialized.type()); + objectPtr, originalType); result = (originalData.size() == loadedData.size()) && diff --git a/Code/Tools/SerializeContextTools/SliceConverter.cpp b/Code/Tools/SerializeContextTools/SliceConverter.cpp index 9fba060960..e7dc49d5b3 100644 --- a/Code/Tools/SerializeContextTools/SliceConverter.cpp +++ b/Code/Tools/SerializeContextTools/SliceConverter.cpp @@ -177,9 +177,10 @@ namespace AZ AZ::Entity* rootEntity = reinterpret_cast(classPtr); bool convertResult = ConvertSliceToPrefab(context, outputPath, isDryRun, rootEntity); - // Clear out the references to any nested slices so that the nested assets get unloaded correctly at the end of - // the conversion. - ClearSliceAssetReferences(rootEntity); + + // Delete the root entity pointer. Otherwise, it will leak itself along with all of the slice asset references held + // within it. + delete rootEntity; return convertResult; }; @@ -229,8 +230,12 @@ namespace AZ return false; } - // Get all of the entities from the slice. + // Get all of the entities from the slice. We're taking ownership of them, so we also remove them from the slice component + // without deleting them. + constexpr bool deleteEntities = false; + constexpr bool removeEmptyInstances = true; SliceComponent::EntityList sliceEntities = sliceComponent->GetNewEntities(); + sliceComponent->RemoveAllEntities(deleteEntities, removeEmptyInstances); AZ_Printf("Convert-Slice", " Slice contains %zu entities.\n", sliceEntities.size()); // Create the Prefab with the entities from the slice. @@ -273,6 +278,12 @@ namespace AZ } } + // Save off a mapping of the slice's metadata entity ID as well, even though we never converted the entity itself. + // This will help us better detect entity ID mapping errors for nested slice instances. + AZ::Entity* metadataEntity = sliceComponent->GetMetadataEntity(); + constexpr bool isMetadataEntity = true; + m_aliasIdMapper.emplace(metadataEntity->GetId(), SliceEntityMappingInfo(templateId, "MetadataEntity", isMetadataEntity)); + // Update the prefab template with the fixed-up data in our prefab instance. AzToolsFramework::Prefab::PrefabDom prefabDom; bool storeResult = AzToolsFramework::Prefab::PrefabDomUtils::StoreInstanceInPrefabDom(*sourceInstance, prefabDom); @@ -402,6 +413,21 @@ namespace AZ "Convert-Slice", " Attaching %zu instances of nested slice '%s'.\n", instances.size(), nestedPrefabPath.Native().c_str()); + // Before processing any further, save off all the known entity IDs from all the instances and how they map back to + // the base nested prefab that they've come from (i.e. this one). As we proceed up the chain of nesting, this will + // build out a hierarchical list of owning instances for each entity that we can trace upwards to know where to add + // the entity into our nested prefab instance. + // This step needs to occur *before* converting the instances themselves, because while converting instances, they + // might have entity ID references that point to other instances. By having the full instance entity ID map in place + // before conversion, we'll be able to fix them up appropriately. + + for (auto& instance : instances) + { + AZStd::string instanceAlias = GetInstanceAlias(instance); + UpdateSliceEntityInstanceMappings(instance.GetEntityIdToBaseMap(), instanceAlias); + } + + // Now that we have all the entity ID mappings, convert all the instances. for (auto& instance : instances) { bool instanceConvertResult = ConvertSliceInstance(instance, sliceAsset, nestedTemplate, sourceInstance); @@ -415,6 +441,28 @@ namespace AZ return true; } + AZStd::string SliceConverter::GetInstanceAlias(const AZ::SliceComponent::SliceInstance& instance) + { + // When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one + // will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce + // the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains + // a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the + // unique instance ID. + AZStd::string instanceAlias; + auto entityIdMap = instance.GetEntityIdMap(); + if (!entityIdMap.empty()) + { + instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str()); + } + else + { + AZ_Error("Convert-Slice", false, " Couldn't create deterministic instance alias."); + instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str()); + } + return instanceAlias; + } + + bool SliceConverter::ConvertSliceInstance( AZ::SliceComponent::SliceInstance& instance, AZ::Data::Asset& sliceAsset, @@ -438,27 +486,7 @@ namespace AZ auto instanceToTemplateInterface = AZ::Interface::Get(); auto prefabSystemComponentInterface = AZ::Interface::Get(); - // When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one - // will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce - // the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains - // a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the - // unique instance ID. - AZStd::string instanceAlias; - auto entityIdMap = instance.GetEntityIdMap(); - if (!entityIdMap.empty()) - { - instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str()); - } - else - { - instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str()); - } - - // Before processing any further, save off all the known entity IDs from this instance and how they map back to the base - // nested prefab that they've come from (i.e. this one). As we proceed up the chain of nesting, this will build out a - // hierarchical list of owning instances for each entity that we can trace upwards to know where to add the entity into - // our nested prefab instance. - UpdateSliceEntityInstanceMappings(instance.GetEntityIdToBaseMap(), instanceAlias); + AZStd::string instanceAlias = GetInstanceAlias(instance); // Create a new unmodified prefab Instance for the nested slice instance. auto nestedInstance = AZStd::make_unique(); @@ -619,6 +647,10 @@ namespace AZ SetParentEntity(containerEntity->get(), topLevelInstance->GetContainerEntityId(), onlySetIfInvalid); } + // After doing all of the above, run through entity references in any of the patched entities, and fix up the entity IDs to + // match the new ones in our prefabs. + RemapIdReferences(m_aliasIdMapper, topLevelInstance, nestedInstance.get(), instantiated, dependentSlice->GetSerializeContext()); + // Add the nested instance itself to the top-level prefab. To do this, we need to add it to our top-level instance, // create a patch out of it, and patch the top-level prefab template. @@ -750,17 +782,6 @@ namespace AZ AZ_Error("Convert-Slice", disconnected, "Asset Processor failed to disconnect successfully."); } - void SliceConverter::ClearSliceAssetReferences(AZ::Entity* rootEntity) - { - SliceComponent* sliceComponent = AZ::EntityUtils::FindFirstDerivedComponent(rootEntity); - // Make a copy of the slice list and remove all of them from the loaded component. - AZ::SliceComponent::SliceList slices = sliceComponent->GetSlices(); - for (auto& slice : slices) - { - sliceComponent->RemoveSlice(&slice); - } - } - void SliceConverter::UpdateSliceEntityInstanceMappings( const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap, const AZStd::string& currentInstanceAlias) { @@ -789,9 +810,108 @@ namespace AZ AZ_Assert(oldId == newId, "The same entity instance ID has unexpectedly appeared twice in the same nested prefab."); } } + else + { + AZ_Warning("Convert-Slice", false, " Couldn't find an entity ID conversion for %s.", oldId.ToString().c_str()); + } } } + void SliceConverter::RemapIdReferences( + const AZStd::unordered_map& idMapper, + AzToolsFramework::Prefab::Instance* topLevelInstance, + AzToolsFramework::Prefab::Instance* nestedInstance, + SliceComponent::InstantiatedContainer* instantiatedEntities, + SerializeContext* context) + { + // Given a set of instantiated entities, run through all of them, look for entity references, and replace the entity IDs with + // new ones that match up with our prefabs. + + IdUtils::Remapper::ReplaceIdsAndIdRefs( + instantiatedEntities, + [idMapper, &topLevelInstance, &nestedInstance]( + const EntityId& sourceId, bool isEntityId, [[maybe_unused]] const AZStd::function& idGenerator) -> EntityId + { + EntityId newId = sourceId; + + // Only convert valid entity references. Actual entity IDs have already been taken care of elsewhere, so ignore them. + if (!isEntityId && sourceId.IsValid()) + { + auto entityEntry = idMapper.find(sourceId); + + // Since we've already remapped transform hierarchies to include container entities, it's possible that our entity + // reference is pointing to a container, which means it won't be in our slice mapping table. In that case, just + // return it as-is. + if (entityEntry == idMapper.end()) + { + return sourceId; + } + + // We've got a slice->prefab mapping entry, so now we need to use it. + auto& mappingStruct = entityEntry->second; + + if (mappingStruct.m_nestedInstanceAliases.empty()) + { + // If we don't have a chain of nested instance aliases, then this entity reference is either within the + // current nested instance or it's pointing to an entity in the top-level instance. We'll try them both + // to look for a match. + + EntityId prefabId = nestedInstance->GetEntityId(mappingStruct.m_entityAlias); + if (!prefabId.IsValid()) + { + prefabId = topLevelInstance->GetEntityId(mappingStruct.m_entityAlias); + } + + if (prefabId.IsValid()) + { + newId = prefabId; + } + else + { + AZ_Error("Convert-Slice", false, " Couldn't find source ID %s", sourceId.ToString().c_str()); + } + } + else + { + // We *do* have a chain of nested instance aliases. This chain could either be relative to the nested instance + // or the top-level instance. We can tell which one it is by which one can find the first nested instance + // alias. + + AzToolsFramework::Prefab::Instance* entityInstance = nestedInstance; + auto it = mappingStruct.m_nestedInstanceAliases.rbegin(); + if (!entityInstance->FindNestedInstance(*it).has_value()) + { + entityInstance = topLevelInstance; + } + + // Now that we've got a starting point, iterate through the chain of nested instance aliases to find the + // correct instance to get the entity ID for. We have to go from slice IDs -> entity aliases -> entity IDs + // because prefab instance creation can change some of our entity IDs along the way. + for (; it != mappingStruct.m_nestedInstanceAliases.rend(); it++) + { + auto foundInstance = entityInstance->FindNestedInstance(*it); + if (foundInstance.has_value()) + { + entityInstance = &(foundInstance->get()); + } + else + { + AZ_Assert(false, "Couldn't find nested instance %s", it->c_str()); + } + } + + EntityId prefabId = entityInstance->GetEntityId(mappingStruct.m_entityAlias); + if (prefabId.IsValid()) + { + newId = prefabId; + } + } + } + + return newId; + }, + context); + } } // namespace SerializeContextTools } // namespace AZ diff --git a/Code/Tools/SerializeContextTools/SliceConverter.h b/Code/Tools/SerializeContextTools/SliceConverter.h index ee0bb0a539..31c8306477 100644 --- a/Code/Tools/SerializeContextTools/SliceConverter.h +++ b/Code/Tools/SerializeContextTools/SliceConverter.h @@ -42,6 +42,28 @@ namespace AZ bool ConvertSliceFiles(Application& application); private: + // When converting slice entities, especially for nested slices, we need to keep track of the original + // entity ID, the entity alias it uses in the prefab, and which template and nested instance path it maps to. + // As we encounter each instanced entity ID, we can look it up in this structure and use this to determine how to properly + // add it to the correct place in the hierarchy. + struct SliceEntityMappingInfo + { + SliceEntityMappingInfo( + AzToolsFramework::Prefab::TemplateId templateId, + AzToolsFramework::Prefab::EntityAlias entityAlias, + bool isMetadataEntity = false) + : m_templateId(templateId) + , m_entityAlias(entityAlias) + , m_isMetadataEntity(isMetadataEntity) + { + } + + AzToolsFramework::Prefab::TemplateId m_templateId; + AzToolsFramework::Prefab::EntityAlias m_entityAlias; + AZStd::vector m_nestedInstanceAliases; + bool m_isMetadataEntity{ false }; + }; + bool ConnectToAssetProcessor(); void DisconnectFromAssetProcessor(); @@ -58,27 +80,17 @@ namespace AZ void SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid); void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId); bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId); - void ClearSliceAssetReferences(AZ::Entity* rootEntity); void UpdateSliceEntityInstanceMappings( const AZ::SliceComponent::EntityIdToEntityIdMap& sliceEntityIdMap, const AZStd::string& currentInstanceAlias); + AZStd::string GetInstanceAlias(const AZ::SliceComponent::SliceInstance& instance); - // When converting slice entities, especially for nested slices, we need to keep track of the original - // entity ID, the entity alias it uses in the prefab, and which template and nested instance path it maps to. - // As we encounter each instanced entity ID, we can look it up in this structure and use this to determine how to properly - // add it to the correct place in the hierarchy. - struct SliceEntityMappingInfo - { - SliceEntityMappingInfo(AzToolsFramework::Prefab::TemplateId templateId, AzToolsFramework::Prefab::EntityAlias entityAlias) - : m_templateId(templateId) - , m_entityAlias(entityAlias) - { - } - - AzToolsFramework::Prefab::TemplateId m_templateId; - AzToolsFramework::Prefab::EntityAlias m_entityAlias; - AZStd::vector m_nestedInstanceAliases; - }; + void RemapIdReferences( + const AZStd::unordered_map& idMapper, + AzToolsFramework::Prefab::Instance* topLevelInstance, + AzToolsFramework::Prefab::Instance* nestedInstance, + SliceComponent::InstantiatedContainer* instantiatedEntities, + SerializeContext* context); // Track all of the entity IDs created and associate them with enough conversion information to know how to place the // entities in the correct place in the prefab hierarchy and fix up parent entity ID mappings to work with the nested diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/CMakeLists.txt b/Code/Tools/TestImpactFramework/Frontend/Console/CMakeLists.txt index 20a680bce9..8298bb7123 100644 --- a/Code/Tools/TestImpactFramework/Frontend/Console/CMakeLists.txt +++ b/Code/Tools/TestImpactFramework/Frontend/Console/CMakeLists.txt @@ -9,4 +9,4 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -add_subdirectory(Code) +add_subdirectory(Code) \ No newline at end of file diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt b/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt index 7a043a30ca..a26814fa51 100644 --- a/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/CMakeLists.txt @@ -9,8 +9,24 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +ly_add_target( + NAME TestImpact.Frontend.Console.Static STATIC + NAMESPACE AZ + FILES_CMAKE + testimpactframework_frontend_console_static_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::TestImpact.Runtime.Static +) + ly_add_target( NAME TestImpact.Frontend.Console EXECUTABLE + OUTPUT_NAME tiaf NAMESPACE AZ FILES_CMAKE testimpactframework_frontend_console_files.cmake @@ -19,5 +35,31 @@ ly_add_target( Source BUILD_DEPENDENCIES PRIVATE - AZ::TestImpact.Runtime.Static + AZ::TestImpact.Frontend.Console.Static ) + +################################################################################ +# Tests +################################################################################ + +# Disbled:SPEC-7246 +#ly_add_target( +# NAME TestImpact.Frontend.Console.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} +# NAMESPACE AZ +# FILES_CMAKE +# testimpactframework_frontend_console_static_tests_files.cmake +# INCLUDE_DIRECTORIES +# PRIVATE +# Include +# Source +# Tests +# BUILD_DEPENDENCIES +# PRIVATE +# AZ::AzTestShared +# AZ::AzTest +# AZ::TestImpact.Frontend.Console.Static +#) +# +#ly_add_googletest( +# NAME AZ::TestImpact.Frontend.Console.Static.Tests +#) diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Include/TestImpactFramework/TestImpactConsoleMain.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Include/TestImpactFramework/TestImpactConsoleMain.h new file mode 100644 index 0000000000..c6ad90a36f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Include/TestImpactFramework/TestImpactConsoleMain.h @@ -0,0 +1,35 @@ +/* + * 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 + +namespace TestImpact +{ + namespace Console + { + enum class ReturnCode : int + { + Success = 0, //!< The instigation operation(s) returned without error. + InvalidArgs, //!< The specified command line arguments were incorrect. + InvalidUnifiedDiff, //!< The specified unified diff could not be transformed into a valid change list. + InvalidConfiguration, //!< The runtime configuration is malformed. + RuntimeError, //!< The runtime encountered an error that it could not recover from. + UnhandledError, //!< The framework encountered an error that it anticipated but did not handle and could not recover from. + UnknownError, //!< An error of unknown origin was encountered that the console or runtime could not recover from. + TestFailure, //!< The test sequence had one or more test failures. + Timeout //!< The test sequence runtime exceeded the global timeout value. + }; + + //! Entry point for the console front end application. + [[nodiscard]] ReturnCode Main(int argc, char** argv); + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.cpp new file mode 100644 index 0000000000..7eff998dda --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.cpp @@ -0,0 +1,455 @@ +/* + * 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 TestImpact +{ + namespace + { + enum + { + // Options + ConfigKey, + ChangeListKey, + OutputChangeListKey, + SequenceKey, + TestPrioritizationPolicyKey, + ExecutionFailurePolicyKey, + FailedTestCoveragePolicyKey, + TestFailurePolicyKey, + IntegrityFailurePolicyKey, + TestShardingPolicyKey, + TargetOutputCaptureKey, + MaxConcurrencyKey, + TestTargetTimeoutKey, + GlobalTimeoutKey, + SuiteFilterKey, + SafeModeKey, + // Values + None, + Seed, + Regular, + ImpactAnalysis, + ImpactAnalysisNoWrite, + ImpactAnalysisOrSeed, + Locality, + Abort, + Continue, + Ignore, + StdOut, + File, + Discard, + Keep + }; + + constexpr const char* OptionKeys[] = + { + // Options + "config", + "changelist", + "ochangelist", + "sequence", + "ppolicy", + "epolicy", + "cpolicy", + "fpolicy", + "ipolicy", + "shard", + "targetout", + "maxconcurrency", + "ttimeout", + "gtimeout", + "suite", + "safemode", + // Values + "none", + "seed", + "regular", + "tia", + "tianowrite", + "tiaorseed", + "locality", + "abort", + "continue", + "ignore", + "stdout", + "file", + "discard", + "keep" + }; + + RepoPath ParseConfigurationFile(const AZ::CommandLine& cmd) + { + return ParsePathOption(OptionKeys[ConfigKey], cmd).value_or(LY_TEST_IMPACT_DEFAULT_CONFIG_FILE); + } + + AZStd::optional ParseChangeListFile(const AZ::CommandLine& cmd) + { + return ParsePathOption(OptionKeys[ChangeListKey], cmd); + } + + bool ParseOutputChangeList(const AZ::CommandLine& cmd) + { + return ParseOnOffOption(OptionKeys[OutputChangeListKey], BinaryStateValue{ false, true }, cmd).value_or(false); + } + + TestSequenceType ParseTestSequenceType(const AZ::CommandLine& cmd) + { + const AZStd::vector> states = + { + {OptionKeys[None], TestSequenceType::None}, + {OptionKeys[Seed], TestSequenceType::Seed}, + {OptionKeys[Regular], TestSequenceType::Regular}, + {OptionKeys[ImpactAnalysis], TestSequenceType::ImpactAnalysis}, + {OptionKeys[ImpactAnalysisNoWrite], TestSequenceType::ImpactAnalysisNoWrite}, + {OptionKeys[ImpactAnalysisOrSeed], TestSequenceType::ImpactAnalysisOrSeed} + }; + + return ParseMultiStateOption(OptionKeys[SequenceKey], states, cmd).value_or(TestSequenceType::None); + } + + Policy::TestPrioritization ParseTestPrioritizationPolicy(const AZ::CommandLine& cmd) + { + const BinaryStateOption states = + { + {OptionKeys[None], Policy::TestPrioritization::None}, + {OptionKeys[Locality], Policy::TestPrioritization::DependencyLocality} + }; + + return ParseBinaryStateOption(OptionKeys[TestPrioritizationPolicyKey], states, cmd).value_or(Policy::TestPrioritization::None); + } + + Policy::ExecutionFailure ParseExecutionFailurePolicy(const AZ::CommandLine& cmd) + { + const AZStd::vector> states = + { + {OptionKeys[Abort], Policy::ExecutionFailure::Abort}, + {OptionKeys[Continue], Policy::ExecutionFailure::Continue}, + {OptionKeys[Ignore], Policy::ExecutionFailure::Ignore} + }; + return ParseMultiStateOption(OptionKeys[ExecutionFailurePolicyKey], states, cmd).value_or(Policy::ExecutionFailure::Continue); + } + + Policy::FailedTestCoverage ParseFailedTestCoveragePolicy(const AZ::CommandLine& cmd) + { + const AZStd::vector> states = + { + {OptionKeys[Discard], Policy::FailedTestCoverage::Discard}, + {OptionKeys[Keep], Policy::FailedTestCoverage::Keep} + }; + + return ParseMultiStateOption(OptionKeys[FailedTestCoveragePolicyKey], states, cmd).value_or(Policy::FailedTestCoverage::Keep); + } + + Policy::TestFailure ParseTestFailurePolicy(const AZ::CommandLine& cmd) + { + const BinaryStateValue states = + { + Policy::TestFailure::Abort, + Policy::TestFailure::Continue + }; + + return ParseAbortContinueOption(OptionKeys[TestFailurePolicyKey], states, cmd).value_or(Policy::TestFailure::Abort); + } + + Policy::IntegrityFailure ParseIntegrityFailurePolicy(const AZ::CommandLine& cmd) + { + const BinaryStateValue states = + { + Policy::IntegrityFailure::Abort, + Policy::IntegrityFailure::Continue + }; + + return ParseAbortContinueOption(OptionKeys[IntegrityFailurePolicyKey], states, cmd).value_or(Policy::IntegrityFailure::Abort); + } + + Policy::TestSharding ParseTestShardingPolicy(const AZ::CommandLine& cmd) + { + const BinaryStateValue states = + { + Policy::TestSharding::Never, + Policy::TestSharding::Always + }; + + return ParseOnOffOption(OptionKeys[TestShardingPolicyKey], states, cmd).value_or(Policy::TestSharding::Never); + } + + Policy::TargetOutputCapture ParseTargetOutputCapture(const AZ::CommandLine& cmd) + { + if (const auto numSwitchValues = cmd.GetNumSwitchValues(OptionKeys[TargetOutputCaptureKey]); + numSwitchValues) + { + AZ_TestImpact_Eval( + numSwitchValues <= 2, CommandLineOptionsException, "Unexpected parameters for target output capture option"); + + Policy::TargetOutputCapture targetOutputCapture = Policy::TargetOutputCapture::None; + for (auto i = 0; i < numSwitchValues; i++) + { + const auto option = cmd.GetSwitchValue(OptionKeys[TargetOutputCaptureKey], i); + if (option == OptionKeys[StdOut]) + { + if (targetOutputCapture == Policy::TargetOutputCapture::File) + { + targetOutputCapture = Policy::TargetOutputCapture::StdOutAndFile; + } + else + { + targetOutputCapture = Policy::TargetOutputCapture::StdOut; + } + } + else if (option == OptionKeys[File]) + { + if (targetOutputCapture == Policy::TargetOutputCapture::StdOut) + { + targetOutputCapture = Policy::TargetOutputCapture::StdOutAndFile; + } + else + { + targetOutputCapture = Policy::TargetOutputCapture::File; + } + } + else + { + throw CommandLineOptionsException( + AZStd::string::format("Unexpected value for target output capture option: %s", option.c_str())); + } + } + + return targetOutputCapture; + } + + return Policy::TargetOutputCapture::None; + } + + AZStd::optional ParseMaxConcurrency(const AZ::CommandLine& cmd) + { + return ParseUnsignedIntegerOption(OptionKeys[MaxConcurrencyKey], cmd); + } + + AZStd::optional ParseTestTargetTimeout(const AZ::CommandLine& cmd) + { + return ParseSecondsOption(OptionKeys[TestTargetTimeoutKey], cmd); + } + + AZStd::optional ParseGlobalTimeout(const AZ::CommandLine& cmd) + { + return ParseSecondsOption(OptionKeys[GlobalTimeoutKey], cmd); + } + + bool ParseSafeMode(const AZ::CommandLine& cmd) + { + const BinaryStateValue states = { false, true }; + return ParseOnOffOption(OptionKeys[SafeModeKey], states, cmd).value_or(false); + } + + SuiteType ParseSuiteFilter(const AZ::CommandLine& cmd) + { + const AZStd::vector> states = + { + {GetSuiteTypeName(SuiteType::Main), SuiteType::Main}, + {GetSuiteTypeName(SuiteType::Periodic), SuiteType::Periodic}, + {GetSuiteTypeName(SuiteType::Sandbox), SuiteType::Sandbox} + }; + + return ParseMultiStateOption(OptionKeys[SuiteFilterKey], states, cmd).value_or(SuiteType::Main); + } + } + + CommandLineOptions::CommandLineOptions(int argc, char** argv) + { + AZ::CommandLine cmd; + cmd.Parse(argc, argv); + + m_configurationFile = ParseConfigurationFile(cmd); + m_changeListFile = ParseChangeListFile(cmd); + m_outputChangeList = ParseOutputChangeList(cmd); + m_testSequenceType = ParseTestSequenceType(cmd); + m_testPrioritizationPolicy = ParseTestPrioritizationPolicy(cmd); + m_executionFailurePolicy = ParseExecutionFailurePolicy(cmd); + m_failedTestCoveragePolicy = ParseFailedTestCoveragePolicy(cmd); + m_testFailurePolicy = ParseTestFailurePolicy(cmd); + m_integrityFailurePolicy = ParseIntegrityFailurePolicy(cmd); + m_testShardingPolicy = ParseTestShardingPolicy(cmd); + m_targetOutputCapture = ParseTargetOutputCapture(cmd); + m_maxConcurrency = ParseMaxConcurrency(cmd); + m_testTargetTimeout = ParseTestTargetTimeout(cmd); + m_globalTimeout = ParseGlobalTimeout(cmd); + m_safeMode = ParseSafeMode(cmd); + m_suiteFilter = ParseSuiteFilter(cmd); + } + + bool CommandLineOptions::HasChangeListFile() const + { + return m_changeListFile.has_value(); + } + + bool CommandLineOptions::HasSafeMode() const + { + return m_safeMode; + } + + const AZStd::optional& CommandLineOptions::GetChangeListFile() const + { + return m_changeListFile; + } + + bool CommandLineOptions::HasOutputChangeList() const + { + return m_outputChangeList; + } + + const RepoPath& CommandLineOptions::GetConfigurationFile() const + { + return m_configurationFile; + } + + TestSequenceType CommandLineOptions::GetTestSequenceType() const + { + return m_testSequenceType; + } + + Policy::TestPrioritization CommandLineOptions::GetTestPrioritizationPolicy() const + { + return m_testPrioritizationPolicy; + } + + Policy::ExecutionFailure CommandLineOptions::GetExecutionFailurePolicy() const + { + return m_executionFailurePolicy; + } + + Policy::FailedTestCoverage CommandLineOptions::GetFailedTestCoveragePolicy() const + { + return m_failedTestCoveragePolicy; + } + + Policy::TestFailure CommandLineOptions::GetTestFailurePolicy() const + { + return m_testFailurePolicy; + } + + Policy::IntegrityFailure CommandLineOptions::GetIntegrityFailurePolicy() const + { + return m_integrityFailurePolicy; + } + + Policy::TestSharding CommandLineOptions::GetTestShardingPolicy() const + { + return m_testShardingPolicy; + } + + Policy::TargetOutputCapture CommandLineOptions::GetTargetOutputCapture() const + { + return m_targetOutputCapture; + } + + const AZStd::optional& CommandLineOptions::GetMaxConcurrency() const + { + return m_maxConcurrency; + } + + const AZStd::optional& CommandLineOptions::GetTestTargetTimeout() const + { + return m_testTargetTimeout; + } + + const AZStd::optional& CommandLineOptions::GetGlobalTimeout() const + { + return m_globalTimeout; + } + + SuiteType CommandLineOptions::GetSuiteFilter() const + { + return m_suiteFilter; + } + + AZStd::string CommandLineOptions::GetCommandLineUsageString() + { + AZStd::string help = + "usage: tiaf [options]\n" + " options:\n" + " -config= Path to the configuration file for the TIAF runtime (default: \n" + " ..json).\n" + " -changelist= Path to the JSON of source file changes to perform test impact \n" + " analysis on.\n" + " -gtimeout= Global timeout value to terminate the entire test sequence should it \n" + " be exceeded.\n" + " -ttimeout= Timeout value to terminate individual test targets should it be \n" + " exceeded.\n" + " -sequence= The type of test sequence to perform, where 'none' runs no tests and\n" + " will report a all tests successful, 'seed' removes any prior coverage \n" + " data and runs all test targets with instrumentation to reseed the \n" + " data from scratch, 'regular' runs all of the test targets without any \n" + " instrumentation to generate coverage data(any prior coverage data is \n" + " left intact), 'tia' uses any prior coverage data to run the instrumented \n" + " subset of selected tests(if no prior coverage data a regular run is \n" + " performed instead), 'tianowrite' uses any prior coverage data to run the \n" + " uninstrumented subset of selected tests (if no prior coverage data a \n" + " regular run is performed instead). The coverage data is not updated with \n" + " the subset of selected tests and 'tiaorseed' uses any prior coverage data \n" + " to run the instrumented subset of selected tests (if no prior coverage \n" + " data a seed run is performed instead).\n" + " -safemode= Flag to specify a safe mode sequence where the set of unselected \n" + " tests is run without instrumentation after the set of selected \n" + " instrumented tests is run (this has the effect of ensuring all \n" + " tests are run regardless).\n" + " -shard= Break any test targets with a sharding policy into the number of \n" + " shards according to the maximum concurrency value.\n" + " -cpolicy= Policy for handling the coverage data of failing tests, where 'discard' \n" + " will discard the coverage data produced by the failing tests, causing \n" + " them to be drafted into future test runs and 'keep' will keep any existing \n" + " coverage data and update the coverage data for failed tests that produce \n" + " coverage.\n" + " -targetout= Capture of individual test run stdout, where 'stdout' will capture \n" + " each individual test target's stdout and output each one to stdout \n" + " and 'file' will capture each individual test target's stdout and output \n" + " each one individually to a file (multiple values are accepted).\n" + " -epolicy= Policy for handling test execution failure (test targets could not be \n" + " launched due to the binary not being built, incorrect paths, etc.), \n" + " where 'abort' will abort the entire test sequence upon the first test\n" + " target execution failure and report a failure(along with the return \n" + " code of the test target that failed to launch), 'continue' will continue \n" + " with the test sequence in the event of test target execution failures\n" + " and treat the test targets that failed to launch as test failures\n" + " (along with the return codes of the test targets that failed to \n" + " launch), 'ignore' will continue with the test sequence in the event of \n" + " test target execution failures and treat the test targets that failed\n" + " to launch as test passes(along with the return codes of the test \n" + " targets that failed to launch).\n" + " -fpolicy Policy for handling test failures (test targets report failing tests), \n" + " where 'abort' will abort the entire test sequence upon the first test \n" + " failure and report a failure and 'continue' will continue with the test\n" + " sequence in the event of test failures and report the test failures.\n" + " -ipolicy= Policy for handling coverage data integrity failures, where 'abort' will \n" + " abort the test sequence and report a failure, 'seed' will attempt another \n" + " sequence using the seed sequence type, otherwise will abort and report \n" + " a failure (this option has no effect for regular and seed sequence \n" + " types) and 'rerun' will attempt another sequence using the regular \n" + " sequence type, otherwise will abort and report a failure(this option has \n" + " no effect for regular sequence type).\n" + " -ppolicy= Policy for prioritizing selected test targets, where 'none' will not \n" + " attempt any test target prioritization and 'locality' will attempt to \n" + " prioritize test targets according to the locality of their covering \n" + " production targets in the dependency graph(if no dependency graph data \n" + " available, no prioritization will occur).\n" + " -maxconcurrency= The maximum number of concurrent test targets/shards to be in flight at \n" + " any given moment.\n" + " -ochangelist= Outputs the change list used for test selection.\n" + " -suite= The test suite to select from for this test sequence."; + + return help; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.h new file mode 100644 index 0000000000..b215261a87 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptions.h @@ -0,0 +1,112 @@ +/* + * 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 TestImpact +{ + //! The type of test sequence to run. + enum class TestSequenceType + { + None, //!< Runs no tests and will report all tests successful. + Seed, //!< Removes any prior coverage data and runs all test targets with instrumentation to reseed the data from scratch. + Regular, //!< Runs all of the test targets without any instrumentation to generate coverage data (any prior coverage data is left intact). + ImpactAnalysis, //!< Uses any prior coverage data to run the instrumented subset of selected tests (if no prior coverage data a regular run is performed instead). + ImpactAnalysisNoWrite, //!< Uses any prior coverage data to run the uninstrumented subset of selected tests (if no prior coverage data a regular run is performed instead). + //!< The coverage data is not updated with the subset of selected tests. + ImpactAnalysisOrSeed //!< Uses any prior coverage data to run the instrumented subset of selected tests (if no prior coverage data a seed run is performed instead). + }; + + //! Representation of the command line options supplied to the console frontend application. + class CommandLineOptions + { + public: + CommandLineOptions(int argc, char** argv); + static AZStd::string GetCommandLineUsageString(); + + //! Returns true if a change list file path has been supplied, otherwise false. + bool HasChangeListFile() const; + + //! Returns true if the safe mode option has been enabled, otherwise false. + bool HasSafeMode() const; + + //! Returns true if the output change list option has been enabled, otherwise false. + bool HasOutputChangeList() const; + + //! Returns the path to the runtime configuration file. + const RepoPath& GetConfigurationFile() const; + + //! Returns the path to the change list file (if any). + const AZStd::optional& GetChangeListFile() const; + + //! Returns the test sequence type to run. + TestSequenceType GetTestSequenceType() const; + + //! Returns the test prioritization policy to use. + Policy::TestPrioritization GetTestPrioritizationPolicy() const; + + //! Returns the test execution failure policy to use. + Policy::ExecutionFailure GetExecutionFailurePolicy() const; + + //! Returns failed test coverage drafting policy to use. + Policy::FailedTestCoverage GetFailedTestCoveragePolicy() const; + + //! Returns the test failure policy to use. + Policy::TestFailure GetTestFailurePolicy() const; + + //! Returns the integration failure policy to use. + Policy::IntegrityFailure GetIntegrityFailurePolicy() const; + + //! Returns the test sharding policy to use. + Policy::TestSharding GetTestShardingPolicy() const; + + //! Returns the test target standard output capture policy to use. + Policy::TargetOutputCapture GetTargetOutputCapture() const; + + //! Returns the maximum number of test targets to be in flight at any given time. + const AZStd::optional& GetMaxConcurrency() const; + + //! Returns the individual test target timeout to use (if any). + const AZStd::optional& GetTestTargetTimeout() const; + + //! Returns the global test sequence timeout to use (if any). + const AZStd::optional& GetGlobalTimeout() const; + + //! Returns the filter for test suite that will be allowed to be run. + SuiteType GetSuiteFilter() const; + + private: + RepoPath m_configurationFile; + AZStd::optional m_changeListFile; + bool m_outputChangeList = false; + TestSequenceType m_testSequenceType; + Policy::TestPrioritization m_testPrioritizationPolicy = Policy::TestPrioritization::None; + Policy::ExecutionFailure m_executionFailurePolicy = Policy::ExecutionFailure::Continue; + Policy::FailedTestCoverage m_failedTestCoveragePolicy = Policy::FailedTestCoverage::Keep; + Policy::TestFailure m_testFailurePolicy = Policy::TestFailure::Abort; + Policy::IntegrityFailure m_integrityFailurePolicy = Policy::IntegrityFailure::Abort; + Policy::TestSharding m_testShardingPolicy = Policy::TestSharding::Never; + Policy::TargetOutputCapture m_targetOutputCapture = Policy::TargetOutputCapture::None; + AZStd::optional m_maxConcurrency; + AZStd::optional m_testTargetTimeout; + AZStd::optional m_globalTimeout; + SuiteType m_suiteFilter; + bool m_safeMode = false; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsException.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsException.h new file mode 100644 index 0000000000..fd0a58b0f5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for command line options. + class CommandLineOptionsException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.cpp new file mode 100644 index 0000000000..b96716059f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.cpp @@ -0,0 +1,79 @@ +/* +* 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 + +namespace TestImpact +{ + //! Attempts to parse a path option value. + AZStd::optional ParsePathOption(const AZStd::string& optionName, const AZ::CommandLine& cmd) + { + if (const auto numSwitchValues = cmd.GetNumSwitchValues(optionName); + numSwitchValues) + { + AZ_TestImpact_Eval( + numSwitchValues == 1, + CommandLineOptionsException, + AZStd::string::format("Unexpected number of parameters for %s option", optionName.c_str())); + + const auto value = cmd.GetSwitchValue(optionName, 0); + AZ_TestImpact_Eval( + !value.empty(), + CommandLineOptionsException, + AZStd::string::format("%s file option value is empty", optionName.c_str())); + + return value; + } + + return AZStd::nullopt; + } + + //! Attempts to pass an unsigned integer option value. + AZStd::optional ParseUnsignedIntegerOption(const AZStd::string& optionName, const AZ::CommandLine& cmd) + { + if (const auto numSwitchValues = cmd.GetNumSwitchValues(optionName); + numSwitchValues) + { + AZ_TestImpact_Eval( + numSwitchValues == 1, + CommandLineOptionsException, + AZStd::string::format("Unexpected number of parameters for %s option", optionName.c_str())); + + const auto strValue = cmd.GetSwitchValue(optionName, 0); + size_t successfulParse = 0; // Will be non-zero if the parse was successful + auto value = AZStd::stoul(strValue, &successfulParse, 0); + + AZ_TestImpact_Eval( + successfulParse, + CommandLineOptionsException, + AZStd::string::format("Couldn't parse unsigned integer option value: %s", strValue.c_str())); + + return aznumeric_caster(value); + } + + return AZStd::nullopt; + } + + //! Attempts to parse an option value in seconds. + AZStd::optional ParseSecondsOption(const AZStd::string& optionName, const AZ::CommandLine& cmd) + { + if (const auto option = ParseUnsignedIntegerOption(optionName, cmd); + option.has_value()) + { + return AZStd::chrono::seconds(option.value()); + } + + return AZStd::nullopt; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.h new file mode 100644 index 0000000000..1708efadd9 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactCommandLineOptionsUtils.h @@ -0,0 +1,122 @@ +/* +* 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 TestImpact +{ + //! Representation of a command line option value name and its typed value. + template + using OptionValue = AZStd::pair; + + //! Representation of a binary state command line option with its two values. + template + using BinaryStateOption = AZStd::pair, OptionValue>; + + //! Representation of the values for a binary state option. + template + using BinaryStateValue = AZStd::pair; + + //! Attempts to parse the specified binary state option. + template + AZStd::optional ParseBinaryStateOption( + const AZStd::string& optionName, + const AZStd::pair, + OptionValue>& state, const AZ::CommandLine& cmd) + { + if (const auto numSwitchValues = cmd.GetNumSwitchValues(optionName); + numSwitchValues) + { + AZ_TestImpact_Eval( + numSwitchValues == 1, + CommandLineOptionsException, + AZStd::string::format("Unexpected number of parameters for %s option", optionName.c_str())); + + const auto option = cmd.GetSwitchValue(optionName, 0); + if (const auto& [optionValueText, optionValue] = state.first; + option == optionValueText) + { + return optionValue; + } + if (const auto& [optionValueText, optionValue] = state.second; + option == optionValueText) + { + return optionValue; + } + + throw CommandLineOptionsException( + AZStd::string::format("Unexpected value for %s option: %s", optionName.c_str(), option.c_str())); + } + + return AZStd::nullopt; + } + + //! Attempts to pass an arbitrarily sized state option. + template + AZStd::optional ParseMultiStateOption( + const AZStd::string& optionName, + const AZStd::vector>& states, + const AZ::CommandLine& cmd) + { + if (const auto numSwitchValues = cmd.GetNumSwitchValues(optionName); + numSwitchValues) + { + AZ_TestImpact_Eval( + numSwitchValues == 1, + CommandLineOptionsException, + AZStd::string::format("Unexpected number of parameters for %s option", optionName.c_str())); + + const auto option = cmd.GetSwitchValue(optionName, 0); + for (const auto& state : states) + { + if (const auto& [optionValueText, optionValue] = state; + option == optionValueText) + { + return optionValue; + } + } + + throw CommandLineOptionsException( + AZStd::string::format("Unexpected value for %s option: %s", optionName.c_str(), option.c_str())); + } + + return AZStd::nullopt; + } + + //! Attempts to pass a specialization of the binary state option where the command line values are "on" and "off". + template + AZStd::optional ParseOnOffOption(const AZStd::string& optionName, const AZStd::pair& states, const AZ::CommandLine& cmd) + { + return ParseBinaryStateOption(optionName, BinaryStateOption{ {"off", states.first}, { "on", states.second } }, cmd); + } + + //! Attempts to pass a specialization of the binary state option where the command line values are "abort" and "continue". + template + AZStd::optional ParseAbortContinueOption(const AZStd::string& optionName, const AZStd::pair& states, const AZ::CommandLine& cmd) + { + return ParseBinaryStateOption(optionName, BinaryStateOption{ {"abort", states.first}, { "continue", states.second } }, cmd); + } + + //! Attempts to parse a path option value. + AZStd::optional ParsePathOption(const AZStd::string& optionName, const AZ::CommandLine& cmd); + + //! Attempts to pass an unsigned integer option value. + AZStd::optional ParseUnsignedIntegerOption(const AZStd::string& optionName, const AZ::CommandLine& cmd); + + //! Attempts to parse an option value in seconds. + AZStd::optional ParseSecondsOption(const AZStd::string& optionName, const AZ::CommandLine& cmd); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsole.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsole.cpp index c20b3c60fe..631eb17b32 100644 --- a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsole.cpp +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsole.cpp @@ -1,17 +1,29 @@ /* -* 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. -* -*/ + * 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. + * + */ -int main([[maybe_unused]] int argc, [[maybe_unused]] char** argv) +#include + +#include +#include + +int main(int argc, char** argv) { - return 0; -} + AZ::AllocatorInstance::Create(); + AZ::AllocatorInstance::Create(); + + TestImpact::Console::ReturnCode returnCode = TestImpact::Console::Main(argc, argv); + AZ::AllocatorInstance::Destroy(); + AZ::AllocatorInstance::Destroy(); + + return static_cast(returnCode); +} diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleMain.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleMain.cpp new file mode 100644 index 0000000000..77b1d98b3c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleMain.cpp @@ -0,0 +1,316 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace TestImpact +{ + namespace Console + { + //! Generates a string to be used for printing to the console for the specified change list. + AZStd::string GenerateChangeListString(const ChangeList& changeList) + { + AZStd::string output; + + const auto& outputFiles = [&output](const AZStd::vector& files) + { + for (const auto& file : files) + { + output += AZStd::string::format("\t%s\n", file.c_str()); + } + }; + + output += AZStd::string::format("Created files (%u):\n", changeList.m_createdFiles.size()); + outputFiles(changeList.m_createdFiles); + + output += AZStd::string::format("Updated files (%u):\n", changeList.m_updatedFiles.size()); + outputFiles(changeList.m_updatedFiles); + + output += AZStd::string::format("Deleted files (%u):\n", changeList.m_deletedFiles.size()); + outputFiles(changeList.m_deletedFiles); + + return output; + } + + //! Gets the appropriate console return code for the specified test sequence result. + ReturnCode GetReturnCodeForTestSequenceResult(TestSequenceResult result) + { + switch (result) + { + case TestSequenceResult::Success: + return ReturnCode::Success; + case TestSequenceResult::Failure: + return ReturnCode::TestFailure; + case TestSequenceResult::Timeout: + return ReturnCode::Timeout; + default: + std::cout << "Unexpected TestSequenceResult value: " << aznumeric_cast(result) << std::endl; + return ReturnCode::UnknownError; + } + } + + //! Wrapper around impact analysis sequences to handle the case where the safe mode option is active. + ReturnCode WrappedImpactAnalysisTestSequence( + TestSequenceEventHandler& sequenceEventHandler, + const CommandLineOptions& options, + Runtime& runtime, + const AZStd::optional& changeList) + { + // Even though it is possible for a regular run to be selected (see below) which does not actually require a change list, + // consider any impact analysis sequence type without a change list to be an error + AZ_TestImpact_Eval( + changeList.has_value(), + CommandLineOptionsException, + "Expected a change list for impact analysis but none was provided"); + + TestSequenceResult result = TestSequenceResult::Failure; + if (options.HasSafeMode()) + { + if (options.GetTestSequenceType() == TestSequenceType::ImpactAnalysis) + { + auto [selectedResult, discardedResult] = runtime.SafeImpactAnalysisTestSequence( + changeList.value(), + options.GetTestPrioritizationPolicy(), + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + + // Handling the possible timeout and failure permutations of the selected and discarded test results is splitting hairs + // so apply the following, admittedly arbitrary, rules to determine what the composite test sequence result should be + if (selectedResult == TestSequenceResult::Success && discardedResult == TestSequenceResult::Success) + { + // Trivial case: both sequences succeeded + result = TestSequenceResult::Success; + } + else if (selectedResult == TestSequenceResult::Failure || discardedResult == TestSequenceResult::Failure) + { + // One sequence failed whilst the other sequence either succeeded or timed out + result = TestSequenceResult::Failure; + } + else + { + // One or both sequences timed out or failed + result = TestSequenceResult::Timeout; + } + } + else if (options.GetTestSequenceType() == TestSequenceType::ImpactAnalysisNoWrite) + { + // A no-write impact analysis sequence with safe mode enabled is functionally identical to a regular sequence type + // due to a) the selected tests being run without instrumentation and b) the discarded tests also being run without + // instrumentation + result = runtime.RegularTestSequence( + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + } + else + { + throw(Exception("Unexpected sequence type")); + } + } + else + { + Policy::DynamicDependencyMap dynamicDependencyMapPolicy; + if (options.GetTestSequenceType() == TestSequenceType::ImpactAnalysis) + { + dynamicDependencyMapPolicy = Policy::DynamicDependencyMap::Update; + } + else if (options.GetTestSequenceType() == TestSequenceType::ImpactAnalysisNoWrite) + { + dynamicDependencyMapPolicy = Policy::DynamicDependencyMap::Discard; + } + else + { + throw(Exception("Unexpected sequence type")); + } + + result = runtime.ImpactAnalysisTestSequence( + changeList.value(), + options.GetTestPrioritizationPolicy(), + dynamicDependencyMapPolicy, + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + } + + return GetReturnCodeForTestSequenceResult(result); + }; + + //! Entry point for the test impact analysis framework console front end application. + ReturnCode Main(int argc, char** argv) + { + try + { + CommandLineOptions options(argc, argv); + AZStd::optional changeList; + + // If we have a change list, check to see whether or not the client has requested the printing of said change list + if (options.HasChangeListFile()) + { + changeList = DeserializeChangeList(ReadFileContents(*options.GetChangeListFile())); + if (options.HasOutputChangeList()) + { + std::cout << "Change List:\n"; + std::cout << GenerateChangeListString(*changeList).c_str(); + + if (options.GetTestSequenceType() == TestSequenceType::None) + { + return ReturnCode::Success; + } + } + } + + // As of now, there are no other non-test operations other than printing a change list so getting this far is considered an error + AZ_TestImpact_Eval(options.GetTestSequenceType() != TestSequenceType::None, CommandLineOptionsException, "No action specified"); + + std::cout << "Constructing in-memory model of source tree and test coverage for test suite "; + std::cout << GetSuiteTypeName(options.GetSuiteFilter()).c_str() << ", this may take a moment...\n"; + Runtime runtime( + RuntimeConfigurationFactory(ReadFileContents(options.GetConfigurationFile())), + options.GetSuiteFilter(), + options.GetExecutionFailurePolicy(), + options.GetFailedTestCoveragePolicy(), + options.GetTestFailurePolicy(), + options.GetIntegrityFailurePolicy(), + options.GetTestShardingPolicy(), + options.GetTargetOutputCapture(), + options.GetMaxConcurrency()); + + if (runtime.HasImpactAnalysisData()) + { + std::cout << "Test impact analysis data for this repository was found.\n"; + } + else + { + std::cout << "Test impact analysis data for this repository was not found, seed or regular sequence fallbacks will be used.\n"; + } + + TestSequenceEventHandler sequenceEventHandler(options.GetSuiteFilter()); + + switch (const auto type = options.GetTestSequenceType()) + { + case TestSequenceType::Regular: + { + const auto result = runtime.RegularTestSequence( + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + + return GetReturnCodeForTestSequenceResult(result); + } + case TestSequenceType::Seed: + { + const auto result = runtime.SeededTestSequence( + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + + return GetReturnCodeForTestSequenceResult(result); + } + case TestSequenceType::ImpactAnalysisNoWrite: + case TestSequenceType::ImpactAnalysis: + { + return WrappedImpactAnalysisTestSequence(sequenceEventHandler, options, runtime, changeList); + } + case TestSequenceType::ImpactAnalysisOrSeed: + { + if (runtime.HasImpactAnalysisData()) + { + return WrappedImpactAnalysisTestSequence(sequenceEventHandler, options, runtime, changeList); + } + else + { + const auto result = runtime.SeededTestSequence( + options.GetTestTargetTimeout(), + options.GetGlobalTimeout(), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler), + AZStd::ref(sequenceEventHandler)); + + return GetReturnCodeForTestSequenceResult(result); + } + } + default: + std::cout << "Unexpected TestSequenceType value: " << static_cast(type) << std::endl; + return ReturnCode::UnknownError; + } + } + catch (const CommandLineOptionsException& e) + { + std::cout << e.what() << std::endl; + std::cout << CommandLineOptions::GetCommandLineUsageString().c_str() << std::endl; + return ReturnCode::InvalidArgs; + } + catch (const ChangeListException& e) + { + std::cout << e.what() << std::endl; + return ReturnCode::InvalidUnifiedDiff; + } + catch (const ConfigurationException& e) + { + std::cout << e.what() << std::endl; + return ReturnCode::InvalidConfiguration; + } + catch (const RuntimeException& e) + { + std::cout << e.what() << std::endl; + return ReturnCode::RuntimeError; + } + catch (const Exception& e) + { + std::cout << e.what() << std::endl; + return ReturnCode::UnhandledError; + } + catch (const std::exception& e) + { + std::cout << e.what() << std::endl; + return ReturnCode::UnknownError; + } + catch (...) + { + std::cout << "An unknown error occurred" << std::endl; + return ReturnCode::UnknownError; + } + } + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.cpp new file mode 100644 index 0000000000..0a745514a5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.cpp @@ -0,0 +1,234 @@ +/* + * 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 TestImpact +{ + namespace Console + { + namespace Output + { + void TestSuiteFilter(SuiteType filter) + { + std::cout << "Test suite filter: " << GetSuiteTypeName(filter).c_str() << "\n"; + } + + void ImpactAnalysisTestSelection(size_t numSelectedTests, size_t numDiscardedTests, size_t numExcludedTests, size_t numDraftedTests) + { + const float totalTests = numSelectedTests + numDiscardedTests; + const float saving = (1.0 - (numSelectedTests / totalTests)) * 100.0f; + + std::cout << numSelectedTests << " tests selected, " << numDiscardedTests << " tests discarded (" << saving << "% test saving)\n"; + std::cout << "Of which " << numExcludedTests << " tests have been excluded and " << numDraftedTests << " tests have been drafted.\n"; + } + + void FailureReport(const Client::SequenceFailure& failureReport, AZStd::chrono::milliseconds duration) + { + std::cout << "Sequence completed in " << (duration.count() / 1000.f) << "s with"; + + if (!failureReport.GetExecutionFailures().empty() || + !failureReport.GetTestRunFailures().empty() || + !failureReport.GetTimedOutTests().empty() || + !failureReport.GetUnexecutedTests().empty()) + { + std::cout << ":\n"; + std::cout << SetColor(Foreground::White, Background::Red).c_str() + << failureReport.GetTestRunFailures().size() + << ResetColor().c_str() << " test failures\n"; + + std::cout << SetColor(Foreground::White, Background::Red).c_str() + << failureReport.GetExecutionFailures().size() + << ResetColor().c_str() << " execution failures\n"; + + std::cout << SetColor(Foreground::White, Background::Red).c_str() + << failureReport.GetTimedOutTests().size() + << ResetColor().c_str() << " test timeouts\n"; + + std::cout << SetColor(Foreground::White, Background::Red).c_str() + << failureReport.GetUnexecutedTests().size() + << ResetColor().c_str() << " unexecuted tests\n"; + + if (!failureReport.GetTestRunFailures().empty()) + { + std::cout << "\nTest failures:\n"; + for (const auto& testRunFailure : failureReport.GetTestRunFailures()) + { + std::cout << " " << testRunFailure.GetTargetName().c_str(); + for (const auto& testCaseFailure : testRunFailure.GetTestCaseFailures()) + { + std::cout << "." << testCaseFailure.GetName().c_str(); + for (const auto& testFailure : testCaseFailure.GetTestFailures()) + { + std::cout << "." << testFailure.GetName().c_str() << "\n"; + } + } + } + } + + if (!failureReport.GetExecutionFailures().empty()) + { + std::cout << "\nExecution failures:\n"; + for (const auto& executionFailure : failureReport.GetExecutionFailures()) + { + std::cout << " " << executionFailure.GetTargetName().c_str() << "\n"; + std::cout << executionFailure.GetCommandString().c_str() << "\n"; + } + } + + if (!failureReport.GetTimedOutTests().empty()) + { + std::cout << "\nTimed out tests:\n"; + for (const auto& testTimeout : failureReport.GetTimedOutTests()) + { + std::cout << " " << testTimeout.GetTargetName().c_str() << "\n"; + } + } + + if (!failureReport.GetUnexecutedTests().empty()) + { + std::cout << "\nUnexecuted tests:\n"; + for (const auto& unexecutedTest : failureReport.GetUnexecutedTests()) + { + std::cout << " " << unexecutedTest.GetTargetName().c_str() << "\n"; + } + } + } + else + { + std::cout << SetColor(Foreground::White, Background::Green).c_str() << " \100% passes!\n" << ResetColor().c_str(); + } + } + } + + TestSequenceEventHandler::TestSequenceEventHandler(SuiteType suiteFilter) + : m_suiteFilter(suiteFilter) + { + } + + // TestSequenceStartCallback + void TestSequenceEventHandler::operator()(Client::TestRunSelection&& selectedTests) + { + ClearState(); + m_numTests = selectedTests.GetNumIncludedTestRuns(); + + Output::TestSuiteFilter(m_suiteFilter); + std::cout << selectedTests.GetNumIncludedTestRuns() << " tests selected, " << selectedTests.GetNumExcludedTestRuns() << " excluded.\n"; + } + + // ImpactAnalysisTestSequenceStartCallback + void TestSequenceEventHandler::operator()( + Client::TestRunSelection&& selectedTests, + AZStd::vector&& discardedTests, + AZStd::vector&& draftedTests) + { + ClearState(); + m_numTests = selectedTests.GetNumIncludedTestRuns() + draftedTests.size(); + + Output::TestSuiteFilter(m_suiteFilter); + Output::ImpactAnalysisTestSelection( + selectedTests.GetTotalNumTests(), discardedTests.size(), selectedTests.GetNumExcludedTestRuns(), draftedTests.size()); + } + + // SafeImpactAnalysisTestSequenceStartCallback + void TestSequenceEventHandler::operator()( + Client::TestRunSelection&& selectedTests, + Client::TestRunSelection&& discardedTests, + AZStd::vector&& draftedTests) + { + ClearState(); + m_numTests = selectedTests.GetNumIncludedTestRuns() + draftedTests.size(); + + Output::TestSuiteFilter(m_suiteFilter); + Output::ImpactAnalysisTestSelection( + selectedTests.GetTotalNumTests(), + discardedTests.GetTotalNumTests(), + selectedTests.GetNumExcludedTestRuns() + discardedTests.GetNumExcludedTestRuns(), + draftedTests.size()); + } + + // TestSequenceCompleteCallback + void TestSequenceEventHandler::operator()( + Client::SequenceFailure&& failureReport, + AZStd::chrono::milliseconds duration) + { + + Output::FailureReport(failureReport, duration); + std::cout << "Updating and serializing the test impact analysis data, this may take a moment...\n"; + } + + // SafeTestSequenceCompleteCallback + void TestSequenceEventHandler::operator()( + Client::SequenceFailure&& selectedFailureReport, + Client::SequenceFailure&& discardedFailureReport, + AZStd::chrono::milliseconds selectedDuration, + AZStd::chrono::milliseconds discaredDuration) + { + std::cout << "Selected test run:\n"; + Output::FailureReport(selectedFailureReport, selectedDuration); + + std::cout << "Discarded test run:\n"; + Output::FailureReport(discardedFailureReport, discaredDuration); + + std::cout << "Updating and serializing the test impact analysis data, this may take a moment...\n"; + } + + // TestRunCompleteCallback + void TestSequenceEventHandler::operator()([[maybe_unused]] Client::TestRun&& test) + { + m_numTestsComplete++; + const auto progress = AZStd::string::format("(%03u/%03u)", m_numTestsComplete, m_numTests, test.GetTargetName().c_str()); + + AZStd::string result; + switch (test.GetResult()) + { + case Client::TestRunResult::AllTestsPass: + { + result = SetColorForString(Foreground::White, Background::Green, "PASS"); + break; + } + case Client::TestRunResult::FailedToExecute: + { + result = SetColorForString(Foreground::White, Background::Red, "EXEC"); + break; + } + case Client::TestRunResult::NotRun: + { + result = SetColorForString(Foreground::White, Background::Yellow, "SKIP"); + break; + } + case Client::TestRunResult::TestFailures: + { + result = SetColorForString(Foreground::White, Background::Red, "FAIL"); + break; + } + case Client::TestRunResult::Timeout: + { + result = SetColorForString(Foreground::White, Background::Magenta, "TIME"); + break; + } + } + + std::cout << progress.c_str() << " " << result.c_str() << " " << test.GetTargetName().c_str() << " (" << (test.GetDuration().count() / 1000.f) << "s)\n"; + } + + void TestSequenceEventHandler::ClearState() + { + m_numTests = 0; + m_numTestsComplete = 0; + } + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.h new file mode 100644 index 0000000000..ee5eee0bf8 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleTestSequenceEventHandler.h @@ -0,0 +1,72 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include +#include + +#include +#include +#include + +#pragma once + +namespace TestImpact +{ + namespace Console + { + //! Event handler for all test sequence types. + class TestSequenceEventHandler + { + public: + explicit TestSequenceEventHandler(SuiteType suiteFilter); + + //! TestSequenceStartCallback. + void operator()(Client::TestRunSelection&& selectedTests); + + //! ImpactAnalysisTestSequenceStartCallback. + void operator()( + Client::TestRunSelection&& selectedTests, + AZStd::vector&& discardedTests, + AZStd::vector&& draftedTests); + + //! SafeImpactAnalysisTestSequenceStartCallback. + void operator()( + Client::TestRunSelection&& selectedTests, + Client::TestRunSelection&& discardedTests, + AZStd::vector&& draftedTests); + + //! TestSequenceCompleteCallback. + void operator()( + Client::SequenceFailure&& failureReport, + AZStd::chrono::milliseconds duration); + + //! SafeTestSequenceCompleteCallback. + void operator()( + Client::SequenceFailure&& selectedFailureReport, + Client::SequenceFailure&& discardedFailureReport, + AZStd::chrono::milliseconds selectedDuration, + AZStd::chrono::milliseconds discaredDuration); + + //! TestRunCompleteCallback. + void operator()(Client::TestRun&& test); + + private: + void ClearState(); + + SuiteType m_suiteFilter; + size_t m_numTests = 0; + size_t m_numTestsComplete = 0; + }; + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.cpp new file mode 100644 index 0000000000..76ce830371 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.cpp @@ -0,0 +1,34 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + namespace Console + { + AZStd::string SetColor(Foreground foreground, Background background) + { + return AZStd::string::format("\033[%u;%um", aznumeric_cast(foreground), aznumeric_cast(background)); + } + + AZStd::string SetColorForString(Foreground foreground, Background background, const AZStd::string& str) + { + return AZStd::string::format("%s%s%s", SetColor(foreground, background).c_str(), str.c_str(), ResetColor().c_str()); + } + + AZStd::string ResetColor() + { + return "\033[0m"; + } + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.h new file mode 100644 index 0000000000..a003e2db26 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactConsoleUtils.h @@ -0,0 +1,56 @@ +/* + * 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 TestImpact +{ + namespace Console + { + //! The set of available foreground colors. + enum class Foreground + { + Black = 30, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White + }; + + //! The set of available background colors. + enum class Background + { + Black = 40, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White + }; + + //! Returns a string to be used to set the specified foreground and background color. + AZStd::string SetColor(Foreground foreground, Background background); + + //! Returns a string with the specified string set to the specified foreground and background color followed by a color reset. + AZStd::string SetColorForString(Foreground foreground, Background background, const AZStd::string& str); + + //! Returns a string to be used to reset the color back to white foreground on black background. + AZStd::string ResetColor(); + } // namespace Console +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp new file mode 100644 index 0000000000..6ef1ce4172 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.cpp @@ -0,0 +1,306 @@ +/* + * 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 + +namespace TestImpact +{ + namespace Config + { + // Keys for pertinent JSON elements + constexpr const char* Keys[] = + { + "root", + "platform", + "relative_paths", + "artifact_dir", + "enumeration_cache_dir", + "test_impact_data_files", + "temp", + "active", + "target_sources", + "static", + "autogen", + "static", + "include_filters", + "input_output_pairer", + "input", + "dir", + "matchers", + "target_dependency_file", + "target_vertex", + "file", + "test_runner", + "instrumentation", + "bin", + "exclude", + "shard", + "fixture_contiguous", + "fixture_interleaved", + "test_contiguous", + "test_interleaved", + "never", + "target", + "policy", + "artifacts", + "meta", + "repo", + "workspace", + "build_target_descriptor", + "dependency_graph_data", + "test_target_meta", + "test_engine", + "target" + }; + + enum + { + Root = 0, + PlatformName, + RelativePaths, + ArtifactDir, + EnumerationCacheDir, + TestImpactDataFiles, + TempWorkspace, + ActiveWorkspace, + TargetSources, + StaticSources, + AutogenSources, + StaticArtifacts, + SourceIncludeFilters, + AutogenInputOutputPairer, + AutogenInputSources, + Directory, + DependencyGraphMatchers, + TargetDependencyFileMatcher, + TargetVertexMatcher, + TestTargetMetaFile, + TestRunner, + TestInstrumentation, + BinaryFile, + TargetExcludeFilter, + TestSharding, + ContinuousFixtureSharding, + InterleavedFixtureSharding, + ContinuousTestSharding, + InterleavedTestSharding, + NeverShard, + TargetName, + TestShardingPolicy, + Artifacts, + Meta, + Repository, + Workspace, + BuildTargetDescriptor, + DependencyGraphData, + TestTargetMeta, + TestEngine, + TargetConfig + }; + } + + //! Returns an absolute path for a path relative to the specified root. + RepoPath GetAbsPathFromRelPath(const RepoPath& root, const RepoPath& rel) + { + return root / rel; + } + + ConfigMeta ParseConfigMeta(const rapidjson::Value& meta) + { + ConfigMeta configMeta; + configMeta.m_platform = meta[Config::Keys[Config::PlatformName]].GetString(); + return configMeta; + } + + RepoConfig ParseRepoConfig(const rapidjson::Value& repo) + { + RepoConfig repoConfig; + repoConfig.m_root = repo[Config::Keys[Config::Root]].GetString(); + return repoConfig; + } + + WorkspaceConfig::Temp ParseTempWorkspaceConfig(const rapidjson::Value& tempWorkspace) + { + WorkspaceConfig::Temp tempWorkspaceConfig; + tempWorkspaceConfig.m_root = tempWorkspace[Config::Keys[Config::Root]].GetString(); + tempWorkspaceConfig.m_artifactDirectory = + GetAbsPathFromRelPath( + tempWorkspaceConfig.m_root, tempWorkspace[Config::Keys[Config::RelativePaths]][Config::Keys[Config::ArtifactDir]].GetString()); + return tempWorkspaceConfig; + } + + AZStd::array ParseTestImpactAnalysisDataFiles(const RepoPath& root, const rapidjson::Value& sparTIAFile) + { + AZStd::array sparTIAFiles; + sparTIAFiles[static_cast(SuiteType::Main)] = + GetAbsPathFromRelPath(root, sparTIAFile[GetSuiteTypeName(SuiteType::Main).c_str()].GetString()); + sparTIAFiles[static_cast(SuiteType::Periodic)] = + GetAbsPathFromRelPath(root, sparTIAFile[GetSuiteTypeName(SuiteType::Periodic).c_str()].GetString()); + sparTIAFiles[static_cast(SuiteType::Sandbox)] = + GetAbsPathFromRelPath(root, sparTIAFile[GetSuiteTypeName(SuiteType::Sandbox).c_str()].GetString()); + + return sparTIAFiles; + } + + WorkspaceConfig::Active ParseActiveWorkspaceConfig(const rapidjson::Value& activeWorkspace) + { + WorkspaceConfig::Active activeWorkspaceConfig; + const auto& relativePaths = activeWorkspace[Config::Keys[Config::RelativePaths]]; + activeWorkspaceConfig.m_root = activeWorkspace[Config::Keys[Config::Root]].GetString(); + activeWorkspaceConfig.m_enumerationCacheDirectory + = GetAbsPathFromRelPath(activeWorkspaceConfig.m_root, relativePaths[Config::Keys[Config::EnumerationCacheDir]].GetString()); + activeWorkspaceConfig.m_sparTIAFiles = + ParseTestImpactAnalysisDataFiles(activeWorkspaceConfig.m_root, relativePaths[Config::Keys[Config::TestImpactDataFiles]]); + return activeWorkspaceConfig; + } + + WorkspaceConfig ParseWorkspaceConfig(const rapidjson::Value& workspace) + { + WorkspaceConfig workspaceConfig; + workspaceConfig.m_temp = ParseTempWorkspaceConfig(workspace[Config::Keys[Config::TempWorkspace]]); + workspaceConfig.m_active = ParseActiveWorkspaceConfig(workspace[Config::Keys[Config::ActiveWorkspace]]); + return workspaceConfig; + } + + BuildTargetDescriptorConfig ParseBuildTargetDescriptorConfig(const rapidjson::Value& buildTargetDescriptor) + { + BuildTargetDescriptorConfig buildTargetDescriptorConfig; + const auto& targetSources = buildTargetDescriptor[Config::Keys[Config::TargetSources]]; + const auto& staticTargetSources = targetSources[Config::Keys[Config::StaticSources]]; + const auto& autogenTargetSources = targetSources[Config::Keys[Config::AutogenSources]]; + buildTargetDescriptorConfig.m_mappingDirectory = buildTargetDescriptor[Config::Keys[Config::Directory]].GetString(); + const auto& staticInclusionFilters = staticTargetSources[Config::Keys[Config::SourceIncludeFilters]].GetArray(); + + buildTargetDescriptorConfig.m_staticInclusionFilters.reserve(staticInclusionFilters.Size()); + for (const auto& staticInclusionFilter : staticInclusionFilters) + { + buildTargetDescriptorConfig.m_staticInclusionFilters.push_back(staticInclusionFilter.GetString()); + } + + buildTargetDescriptorConfig.m_inputOutputPairer = autogenTargetSources[Config::Keys[Config::AutogenInputOutputPairer]].GetString(); + const auto& inputInclusionFilters = + autogenTargetSources[Config::Keys[Config::AutogenInputSources]][Config::Keys[Config::SourceIncludeFilters]].GetArray(); + buildTargetDescriptorConfig.m_inputInclusionFilters.reserve(inputInclusionFilters.Size()); + for (const auto& inputInclusionFilter : inputInclusionFilters) + { + buildTargetDescriptorConfig.m_inputInclusionFilters.push_back(inputInclusionFilter.GetString()); + } + + return buildTargetDescriptorConfig; + } + + DependencyGraphDataConfig ParseDependencyGraphDataConfig(const rapidjson::Value& dependencyGraphData) + { + DependencyGraphDataConfig dependencyGraphDataConfig; + const auto& matchers = dependencyGraphData[Config::Keys[Config::DependencyGraphMatchers]]; + dependencyGraphDataConfig.m_graphDirectory = dependencyGraphData[Config::Keys[Config::Directory]].GetString(); + dependencyGraphDataConfig.m_targetDependencyFileMatcher = matchers[Config::Keys[Config::TargetDependencyFileMatcher]].GetString(); + dependencyGraphDataConfig.m_targetVertexMatcher = matchers[Config::Keys[Config::TargetVertexMatcher]].GetString(); + return dependencyGraphDataConfig; + } + + TestTargetMetaConfig ParseTestTargetMetaConfig(const rapidjson::Value& testTargetMeta) + { + TestTargetMetaConfig testTargetMetaConfig; + testTargetMetaConfig.m_metaFile = testTargetMeta[Config::Keys[Config::TestTargetMetaFile]].GetString(); + return testTargetMetaConfig; + } + + TestEngineConfig ParseTestEngineConfig(const rapidjson::Value& testEngine) + { + TestEngineConfig testEngineConfig; + testEngineConfig.m_testRunner.m_binary = testEngine[Config::Keys[Config::TestRunner]][Config::Keys[Config::BinaryFile]].GetString(); + testEngineConfig.m_instrumentation.m_binary = testEngine[Config::Keys[Config::TestInstrumentation]][Config::Keys[Config::BinaryFile]].GetString(); + return testEngineConfig; + } + + TargetConfig ParseTargetConfig(const rapidjson::Value& target) + { + TargetConfig targetConfig; + targetConfig.m_outputDirectory = target[Config::Keys[Config::Directory]].GetString(); + const auto& testExcludes = target[Config::Keys[Config::TargetExcludeFilter]].GetArray(); + targetConfig.m_excludedTestTargets.reserve(testExcludes.Size()); + for (const auto& testExclude : testExcludes) + { + targetConfig.m_excludedTestTargets.push_back(testExclude.GetString()); + } + + const auto& testShards = target[Config::Keys[Config::TestSharding]].GetArray(); + targetConfig.m_shardedTestTargets.reserve(testShards.Size()); + for (const auto& testShard : testShards) + { + const auto getShardingConfiguration = [](const AZStd::string& config) + { + if (config == Config::Keys[Config::ContinuousFixtureSharding]) + { + return ShardConfiguration::FixtureContiguous; + } + else if (config == Config::Keys[Config::InterleavedFixtureSharding]) + { + return ShardConfiguration::FixtureInterleaved; + } + else if (config == Config::Keys[Config::ContinuousTestSharding]) + { + return ShardConfiguration::TestContiguous; + } + else if (config == Config::Keys[Config::InterleavedTestSharding]) + { + return ShardConfiguration::TestInterleaved; + } + else if (config == Config::Keys[Config::NeverShard]) + { + return ShardConfiguration::Never; + } + else + { + throw ConfigurationException(AZStd::string::format("Unexpected sharding configuration: %s", config.c_str())); + } + }; + + TargetConfig::ShardedTarget shard; + shard.m_name = testShard[Config::Keys[Config::TargetName]].GetString(); + shard.m_configuration = getShardingConfiguration(testShard[Config::Keys[Config::TestShardingPolicy]].GetString()); + targetConfig.m_shardedTestTargets.push_back(AZStd::move(shard)); + } + + return targetConfig; + } + + RuntimeConfig RuntimeConfigurationFactory(const AZStd::string& configurationData) + { + rapidjson::Document configurationFile; + + if (configurationFile.Parse(configurationData.c_str()).HasParseError()) + { + throw TestImpact::ConfigurationException("Could not parse runtimeConfig data, JSON has errors"); + } + + RuntimeConfig runtimeConfig; + const auto& staticArtifacts = configurationFile[Config::Keys[Config::Artifacts]][Config::Keys[Config::StaticArtifacts]]; + runtimeConfig.m_meta = ParseConfigMeta(configurationFile[Config::Keys[Config::Meta]]); + runtimeConfig.m_repo = ParseRepoConfig(configurationFile[Config::Keys[Config::Repository]]); + runtimeConfig.m_workspace = ParseWorkspaceConfig(configurationFile[Config::Keys[Config::Workspace]]); + runtimeConfig.m_buildTargetDescriptor = ParseBuildTargetDescriptorConfig(staticArtifacts[Config::Keys[Config::BuildTargetDescriptor]]); + runtimeConfig.m_dependencyGraphData = ParseDependencyGraphDataConfig(staticArtifacts[Config::Keys[Config::DependencyGraphData]]); + runtimeConfig.m_testTargetMeta = ParseTestTargetMetaConfig(staticArtifacts[Config::Keys[Config::TestTargetMeta]]); + runtimeConfig.m_testEngine = ParseTestEngineConfig(configurationFile[Config::Keys[Config::TestEngine]]); + runtimeConfig.m_target = ParseTargetConfig(configurationFile[Config::Keys[Config::TargetConfig]]); + + return runtimeConfig; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.h b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.h new file mode 100644 index 0000000000..ac7dbbac94 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/Source/TestImpactRuntimeConfigurationFactory.h @@ -0,0 +1,19 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + //! Parses the configuration data (in JSON format) and returns the constructed runtime configuration. + RuntimeConfig RuntimeConfigurationFactory(const AZStd::string& configurationData); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_files.cmake b/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_files.cmake new file mode 100644 index 0000000000..97081dc08c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_files.cmake @@ -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. +# + +set(FILES + Include/TestImpactFramework/TestImpactConsoleMain.h + Source/TestImpactCommandLineOptions.h + Source/TestImpactCommandLineOptions.cpp + Source/TestImpactCommandLineOptionsUtils.cpp + Source/TestImpactCommandLineOptionsUtils.h + Source/TestImpactCommandLineOptionsException.h + Source/TestImpactRuntimeConfigurationFactory.h + Source/TestImpactRuntimeConfigurationFactory.cpp + Source/TestImpactConsoleMain.cpp + Source/TestImpactConsoleTestSequenceEventHandler.cpp + Source/TestImpactConsoleTestSequenceEventHandler.h + Source/TestImpactConsoleUtils.cpp + Source/TestImpactConsoleUtils.h +) diff --git a/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_tests_files.cmake b/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_tests_files.cmake new file mode 100644 index 0000000000..5714be5dfb --- /dev/null +++ b/Code/Tools/TestImpactFramework/Frontend/Console/Code/testimpactframework_frontend_console_static_tests_files.cmake @@ -0,0 +1,13 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES +) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/CMakeLists.txt b/Code/Tools/TestImpactFramework/Runtime/Code/CMakeLists.txt index 404e8f1cc3..e89dfe60d7 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/CMakeLists.txt +++ b/Code/Tools/TestImpactFramework/Runtime/Code/CMakeLists.txt @@ -9,8 +9,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # - ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME}) +ly_get_list_relative_pal_filename(common_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/Common) ly_add_target( NAME TestImpact.Runtime.Static STATIC @@ -18,12 +18,71 @@ ly_add_target( FILES_CMAKE testimpactframework_runtime_files.cmake ${pal_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake + PLATFORM_INCLUDE_FILES + ${common_dir}/${PAL_TRAIT_COMPILER_ID}/testimpactframework_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake INCLUDE_DIRECTORIES PRIVATE Source PUBLIC Include BUILD_DEPENDENCIES - Public + PUBLIC AZ::AzCore ) + +################################################################################ +# Tests +################################################################################ + +# Disabled: SPEC-7246 +#add_subdirectory(Tests/TestProcess) +#add_subdirectory(Tests/TestTargetA) +#add_subdirectory(Tests/TestTargetB) +#add_subdirectory(Tests/TestTargetC) +#add_subdirectory(Tests/TestTargetD) +# +#ly_add_target( +# NAME TestImpact.Runtime.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} +# NAMESPACE AZ +# FILES_CMAKE +# testimpactframework_runtime_tests_files.cmake +# INCLUDE_DIRECTORIES +# PRIVATE +# Include +# Source +# Tests +# BUILD_DEPENDENCIES +# PRIVATE +# AZ::AzTestShared +# AZ::AzTest +# AZ::TestImpact.Runtime.Static +# RUNTIME_DEPENDENCIES +# AZ::AzTestRunner +# AZ::TestImpact.TestProcess.Console +# AZ::TestImpact.TestTargetA.Tests +# AZ::TestImpact.TestTargetB.Tests +# AZ::TestImpact.TestTargetC.Tests +# AZ::TestImpact.TestTargetD.Tests +# COMPILE_DEFINITIONS +# PRIVATE +# LY_TEST_IMPACT_AZ_TESTRUNNER_BIN="$" +# LY_TEST_IMPACT_TEST_PROCESS_BIN="$" +# LY_TEST_IMPACT_TEST_TARGET_A_BIN="$" +# LY_TEST_IMPACT_TEST_TARGET_B_BIN="$" +# LY_TEST_IMPACT_TEST_TARGET_C_BIN="$" +# LY_TEST_IMPACT_TEST_TARGET_D_BIN="$" +# LY_TEST_IMPACT_TEST_TARGET_A_BASE_NAME="$" +# LY_TEST_IMPACT_TEST_TARGET_B_BASE_NAME="$" +# LY_TEST_IMPACT_TEST_TARGET_C_BASE_NAME="$" +# LY_TEST_IMPACT_TEST_TARGET_D_BASE_NAME="$" +# LY_TEST_IMPACT_TEST_TARGET_ENUMERATION_DIR="${GTEST_XML_OUTPUT_DIR}/TestImpact/Temp/Exclusive/Enum" +# LY_TEST_IMPACT_TEST_TARGET_RESULTS_DIR="${GTEST_XML_OUTPUT_DIR}/TestImpact/Temp/Exclusive/Result" +# LY_TEST_IMPACT_TEST_TARGET_COVERAGE_DIR="${GTEST_XML_OUTPUT_DIR}/TestImpact/Temp/Exclusive/Coverage" +# LY_TEST_IMPACT_INSTRUMENTATION_BIN="${LY_TEST_IMPACT_INSTRUMENTATION_BIN}" +# LY_TEST_IMPACT_MODULES_DIR="${CMAKE_BINARY_DIR}" +# LY_TEST_IMPACT_COVERAGE_SOURCES_DIR="${CMAKE_CURRENT_SOURCE_DIR}" +#) +# +#ly_add_googletest( +# NAME AZ::TestImpact.Runtime.Tests +#) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeList.h new file mode 100644 index 0000000000..e36b343544 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeList.h @@ -0,0 +1,28 @@ +/* + * 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 + +namespace TestImpact +{ + //! Representation of the file CRUD operations of a given set of source changes. + struct ChangeList + { + AZStd::vector m_createdFiles; //!< Files that were newly created. + AZStd::vector m_updatedFiles; //!< Files that were updated. + AZStd::vector m_deletedFiles; //!< Files that were deleted. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListException.h new file mode 100644 index 0000000000..d9139cf47f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for change list operations. + class ChangeListException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListSerializer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListSerializer.h new file mode 100644 index 0000000000..13c41db564 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactChangeListSerializer.h @@ -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 + +#include + +#include + +namespace TestImpact +{ + //! Serializes the specified change list to JSON format. + AZStd::string SerializeChangeList(const ChangeList& changeList); + + //! Deserializes a change list from the specified test run data in JSON format. + ChangeList DeserializeChangeList(const AZStd::string& changeListString); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientFailureReport.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientFailureReport.h new file mode 100644 index 0000000000..0a482e8c51 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientFailureReport.h @@ -0,0 +1,129 @@ +/* + * 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 + +namespace TestImpact +{ + namespace Client + { + //! Represents a test target that failed, either due to failing to execute, completing in an abnormal state or completing with failing tests. + class TargetFailure + { + public: + TargetFailure(const AZStd::string& targetName); + + //! Returns the name of the test target this failure pertains to. + const AZStd::string& GetTargetName() const; + private: + AZStd::string m_targetName; + }; + + //! Represents a test target that failed to execute. + class ExecutionFailure + : public TargetFailure + { + public: + ExecutionFailure(const AZStd::string& targetName, const AZStd::string& command); + + //! Returns the command string used to execute this test target. + const AZStd::string& GetCommandString() const; + private: + AZStd::string m_commandString; + }; + + //! Represents an individual test of a test target that failed. + class TestFailure + { + public: + TestFailure(const AZStd::string& testName, const AZStd::string& errorMessage); + + //! Returns the name of the test that failed. + const AZStd::string& GetName() const; + + //! Returns the error message of the test that failed. + const AZStd::string& GetErrorMessage() const; + + private: + AZStd::string m_name; + AZStd::string m_errorMessage; + }; + + //! Represents a collection of tests that failed. + //! @note Only the failing tests are included in the collection. + class TestCaseFailure + { + public: + TestCaseFailure(const AZStd::string& testCaseName, AZStd::vector&& testFailures); + + //! Returns the name of the test case containing the failing tests. + const AZStd::string& GetName() const; + + //! Returns the collection of tests in this test case that failed. + const AZStd::vector& GetTestFailures() const; + + private: + AZStd::string m_name; + AZStd::vector m_testFailures; + }; + + //! Represents a test target that launched successfully but contains failing tests. + class TestRunFailure + : public TargetFailure + { + public: + TestRunFailure(const AZStd::string& targetName, AZStd::vector&& testFailures); + + //! Returns the total number of failing tests in this run. + size_t GetNumTestFailures() const; + + //! Returns the test cases in this run containing failing tests. + const AZStd::vector& GetTestCaseFailures() const; + + private: + AZStd::vector m_testCaseFailures; + size_t m_numTestFailures = 0; + }; + + //! Base class for reporting failing test sequences. + class SequenceFailure + { + public: + SequenceFailure( + AZStd::vector&& executionFailures, + AZStd::vector&& testRunFailures, + AZStd::vector&& timedOutTests, + AZStd::vector&& unexecutedTests); + + //! Returns the test targets in this sequence that failed to execute. + const AZStd::vector& GetExecutionFailures() const; + + //! Returns the test targets that contain failing tests. + const AZStd::vector& GetTestRunFailures() const; + + //! Returns the test targets in this sequence that were terminated for exceeding their allotted runtime. + const AZStd::vector& GetTimedOutTests() const; + + //! Returns the test targets in this sequence that were not executed due to the sequence terminating prematurely. + const AZStd::vector& GetUnexecutedTests() const; + + private: + AZStd::vector m_executionFailures; + AZStd::vector m_testRunFailures; + AZStd::vector m_timedOutTests; + AZStd::vector m_unexecutedTests; + }; + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestRun.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestRun.h new file mode 100644 index 0000000000..7c525e00ba --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestRun.h @@ -0,0 +1,46 @@ +/* + * 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 + +#pragma once + +namespace TestImpact +{ + namespace Client + { + //! Result of a test run. + enum class TestRunResult + { + NotRun, //!< The test run was not executed due to the test sequence terminating prematurely. + FailedToExecute, //!< The test run failed to execute either due to the target binary missing or incorrect arguments. + Timeout, //!< The test run timed out whilst in flight before being able to complete its run. + TestFailures, //!< The test run completed its run but there were failing tests. + AllTestsPass //!< The test run completed its run and all tests passed. + }; + + class TestRun + { + public: + TestRun(const AZStd::string& name, TestRunResult result, AZStd::chrono::milliseconds duration); + const AZStd::string& GetTargetName() const; + TestRunResult GetResult() const; + AZStd::chrono::milliseconds GetDuration() const; + + private: + AZStd::string m_targetName; + TestRunResult m_result; + AZStd::chrono::milliseconds m_duration; + }; + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestSelection.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestSelection.h new file mode 100644 index 0000000000..7720bbc5ad --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactClientTestSelection.h @@ -0,0 +1,51 @@ +/* + * 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 + +#pragma once + +namespace TestImpact +{ + namespace Client + { + //! The set of test targets selected to run regardless of whether or not the test targets are to be excluded either for being on the primary exclude + //! list and/or being part of a test suite excluded from this run. + //! @note Only the included test targets will be run. The excluded test targets, although selected, will not be run. + class TestRunSelection + { + public: + TestRunSelection(const AZStd::vector& includedTests, const AZStd::vector& excludedTests); + TestRunSelection(AZStd::vector&& includedTests, AZStd::vector&& excludedTests); + + //! Returns the test runs that were selected to be run and will actually be run. + const AZStd::vector& GetIncludededTestRuns() const; + + //! Returns the test runs that were selected to be run but will not actually be run. + const AZStd::vector& GetExcludedTestRuns() const; + + //! Returns the number of selected test runs that will be run. + size_t GetNumIncludedTestRuns() const; + + //! Returns the number of selected test runs that will not be run. + size_t GetNumExcludedTestRuns() const; + + //! Returns the total number of test runs selected regardless of whether or not they will actually be run. + size_t GetTotalNumTests() const; + + private: + AZStd::vector m_includedTestRuns; + AZStd::vector m_excludedTestRuns; + }; + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h new file mode 100644 index 0000000000..ee73ac572d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfiguration.h @@ -0,0 +1,126 @@ +/* + * 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 TestImpact +{ + //! Meta-data about the configuration. + struct ConfigMeta + { + AZStd::string m_platform; //!< The platform for which the configuration pertains to. + }; + + //! Repository configuration. + struct RepoConfig + { + RepoPath m_root; //!< The absolute path to the repository root. + }; + + //! Test impact analysis framework workspace configuration. + struct WorkspaceConfig + { + //! Temporary workspace configuration. + struct Temp + { + RepoPath m_root; //!< Path to the temporary workspace (cleaned prior to use). + RepoPath m_artifactDirectory; //!< Path to read and write runtime artifacts to and from. + }; + + //! Active persistent data workspace configuration. + struct Active + { + RepoPath m_root; //!< Path to the persistent workspace tracked by the repository. + RepoPath m_enumerationCacheDirectory; //!< Path to the test enumerations cache. + AZStd::array m_sparTIAFiles; //!< Paths to the test impact analysis data files for each test suite. + }; + + Temp m_temp; + Active m_active; + }; + + //! Build target descriptor configuration. + struct BuildTargetDescriptorConfig + { + RepoPath m_mappingDirectory; //!< Path to the source to target mapping files. + AZStd::vector m_staticInclusionFilters; //!< File extensions to include for static files. + AZStd::string m_inputOutputPairer; //!< Regex for matching autogen input files with autogen outputs files. + AZStd::vector m_inputInclusionFilters; //!< File extensions fo include for autogen input files. + }; + + //! Dependency graph configuration. + struct DependencyGraphDataConfig + { + RepoPath m_graphDirectory; //!< Path to the dependency graph files. + AZStd::string m_targetDependencyFileMatcher; //!< Regex for matching dependency graph files to build targets. + AZStd::string m_targetVertexMatcher; //!< Regex form matching dependency graph vertices to build targets. + }; + + //! Test target meta configuration. + struct TestTargetMetaConfig + { + RepoPath m_metaFile; //!< Path to the test target meta file. + }; + + //! Test engine configuration. + struct TestEngineConfig + { + //! Test runner configuration. + struct TestRunner + { + RepoPath m_binary; //!< Path to the test runner binary. + }; + + //! Test instrumentation configuration. + struct Instrumentation + { + RepoPath m_binary; //!< Path to the test instrumentation binary. + }; + + TestRunner m_testRunner; + Instrumentation m_instrumentation; + }; + + //! Build target configuration. + struct TargetConfig + { + //! Test target sharding configuration. + struct ShardedTarget + { + AZStd::string m_name; //!< Name of test target this sharding configuration applies to. + ShardConfiguration m_configuration; //!< The shard configuration to use. + }; + + RepoPath m_outputDirectory; //!< Path to the test target binary directory. + AZStd::vector m_excludedTestTargets; //!< Test targets to always exclude from test run sequences. + AZStd::vector m_shardedTestTargets; //!< Test target shard configurations (opt-in). + }; + + struct RuntimeConfig + { + ConfigMeta m_meta; + RepoConfig m_repo; + WorkspaceConfig m_workspace; + BuildTargetDescriptorConfig m_buildTargetDescriptor; + DependencyGraphDataConfig m_dependencyGraphData; + TestTargetMetaConfig m_testTargetMeta; + TestEngineConfig m_testEngine; + TargetConfig m_target; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfigurationException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfigurationException.h new file mode 100644 index 0000000000..15d7913997 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactConfigurationException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for configuration operations. + class ConfigurationException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactException.h new file mode 100644 index 0000000000..83ba48aa46 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactException.h @@ -0,0 +1,51 @@ +/* + * 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 + +//! Evaluates the specified condition and throws the specified exception with the specified +// !message upon failure. +#define AZ_TestImpact_Eval(CONDITION, EXCEPTION_TYPE, MSG) \ + do \ + { \ + static_assert( \ + AZStd::is_base_of_v, \ + "TestImpact Eval macro must only be used with TestImpact exceptions"); \ + if(!(CONDITION)) \ + { \ + throw(EXCEPTION_TYPE(MSG)); \ + } \ + } \ + while (0) + +namespace TestImpact +{ + //! Base class for test impact framework exceptions. + //! @note The message passed in to the constructor is copied and thus safe with dynamic strings. + class Exception + : public std::exception + { + public: + explicit Exception() = default; + explicit Exception(const AZStd::string& msg); + explicit Exception(const char* msg); + const char* what() const noexcept override; + + private: + //! Error message detailing the reason for the exception. + AZStd::string m_msg; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactFileUtils.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactFileUtils.h new file mode 100644 index 0000000000..ffecd27c23 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactFileUtils.h @@ -0,0 +1,85 @@ +/* + * 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 + +#pragma once + +namespace TestImpact +{ + //! Attempts to read the contents of the specified file into a string. + //! @tparam ExceptionType The exception type to throw upon failure. + //! @param path The path to the file to read the contents of. + //! @returns The contents of the file. + template + AZStd::string ReadFileContents(const RepoPath& path) + { + const auto fileSize = AZ::IO::SystemFile::Length(path.c_str()); + AZ_TestImpact_Eval(fileSize > 0, ExceptionType, AZStd::string::format("File %s does not exist", path.c_str())); + + AZStd::vector buffer(fileSize + 1); + buffer[fileSize] = '\0'; + AZ_TestImpact_Eval( + AZ::IO::SystemFile::Read(path.c_str(), buffer.data()), + ExceptionType, + AZStd::string::format("Could not read contents of file %s", path.c_str())); + + return AZStd::string(buffer.begin(), buffer.end()); + } + + //! Attempts to write the contents of the specified string to a file. + //! @tparam ExceptionType The exception type to throw upon failure. + //! @param contents The contents to write to the file. + //! @param path The path to the file to write the contents to. + template + void WriteFileContents(const AZStd::string& contents, const RepoPath& path) + { + AZ::IO::SystemFile file; + const AZStd::vector bytes(contents.begin(), contents.end()); + AZ_TestImpact_Eval( + file.Open(path.c_str(), + AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY), + ExceptionType, + AZStd::string::format("Couldn't open file %s for writing", path.c_str())); + + AZ_TestImpact_Eval( + file.Write(bytes.data(), bytes.size()), ExceptionType, AZStd::string::format("Couldn't write contents for file %s", path.c_str())); + } + + //! Delete the files that match the pattern from the specified directory. + //! @param path The path to the directory to pattern match the files for deletion. + //! @param pattern The pattern to match files for deletion. + inline void DeleteFiles(const RepoPath& path, const AZStd::string& pattern) + { + AZ::IO::SystemFile::FindFiles(AZStd::string::format("%s/%s", path.c_str(), pattern.c_str()).c_str(), + [&path](const char* file, bool isFile) + { + if (isFile) + { + AZ::IO::SystemFile::Delete(AZStd::string::format("%s/%s", path.c_str(), file).c_str()); + } + + return true; + }); + } + + //! Deletes the specified file. + inline void DeleteFile(const RepoPath& file) + { + DeleteFiles(file.ParentPath(), file.Filename().Native()); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRepoPath.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRepoPath.h new file mode 100644 index 0000000000..9ac457e428 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRepoPath.h @@ -0,0 +1,98 @@ +/* + * 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 + +namespace TestImpact +{ + //! Wrapper class to ensure that all paths have the same path separator regardless of how they are sourced. This is critical + //! to the test impact analysis data as otherwise querying/retrieving test impact analysis data for the same source albeit + //! with different path separators will be considered different files entirely. + class RepoPath + { + public: + using string_type = AZ::IO::Path::string_type; + using string_view_type = AZ::IO::Path::string_view_type; + using value_type = AZ::IO::Path::value_type; + + constexpr RepoPath() = default; + constexpr RepoPath(const RepoPath&) = default; + constexpr RepoPath(RepoPath&&) noexcept = default; + constexpr RepoPath::RepoPath(const string_type& path) noexcept; + constexpr RepoPath::RepoPath(const string_view_type& path) noexcept; + constexpr RepoPath::RepoPath(const value_type* path) noexcept; + constexpr RepoPath::RepoPath(const AZ::IO::PathView& path); + constexpr RepoPath::RepoPath(const AZ::IO::Path& path); + + RepoPath& operator=(const RepoPath&) noexcept = default; + RepoPath& operator=(const string_type&) noexcept; + RepoPath& operator=(const value_type*) noexcept; + RepoPath& operator=(const AZ::IO::Path& str) noexcept; + + const char* c_str() const { return m_path.c_str(); } + AZStd::string String() const { return m_path.String(); } + constexpr AZ::IO::PathView Stem() const { return m_path.Stem(); } + constexpr AZ::IO::PathView Extension() const { return m_path.Extension(); } + constexpr bool empty() const { return m_path.empty(); } + constexpr AZ::IO::PathView ParentPath() const { return m_path.ParentPath(); } + constexpr AZ::IO::PathView Filename() const { return m_path.Filename(); } + AZ::IO::Path LexicallyRelative(const RepoPath& base) const { return m_path.LexicallyRelative(base.m_path); } + [[nodiscard]] bool IsRelativeTo(const RepoPath& base) const { return m_path.IsRelativeTo(base.m_path); } + constexpr AZ::IO::PathView RootName() const { return m_path.RootName(); } + constexpr AZ::IO::PathView RelativePath() const { return m_path.RelativePath(); } + + // Wrappers around the AZ::IO::Path concatenation operator + friend RepoPath operator/(const RepoPath& lhs, const AZ::IO::PathView& rhs); + friend RepoPath operator/(const RepoPath& lhs, AZStd::string_view rhs); + friend RepoPath operator/(const RepoPath& lhs, const typename value_type* rhs); + friend RepoPath operator/(const RepoPath& lhs, const RepoPath& rhs); + RepoPath& operator/=(const AZ::IO::PathView& rhs); + RepoPath& operator/=(AZStd::string_view rhs); + RepoPath& operator/=(const typename value_type* rhs); + RepoPath& operator/=(const RepoPath& rhs); + + friend bool operator==(const RepoPath& lhs, const RepoPath& rhs) noexcept; + friend bool operator!=(const RepoPath& lhs, const RepoPath& rhs) noexcept; + friend bool operator<(const RepoPath& lhs, const RepoPath& rhs) noexcept; + + private: + AZ::IO::Path m_path; + }; + + constexpr RepoPath::RepoPath(const string_type& path) noexcept + : m_path(AZ::IO::Path(path).MakePreferred()) + { + } + + constexpr RepoPath::RepoPath(const string_view_type& path) noexcept + : m_path(AZ::IO::Path(path).MakePreferred()) + { + } + + constexpr RepoPath::RepoPath(const value_type* path) noexcept + : m_path(AZ::IO::Path(path).MakePreferred()) + { + } + + constexpr RepoPath::RepoPath(const AZ::IO::PathView& path) + : m_path(AZ::IO::Path(path).MakePreferred()) + { + } + + constexpr RepoPath::RepoPath(const AZ::IO::Path& path) + : m_path(AZ::IO::Path(path).MakePreferred()) + { + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntime.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntime.h new file mode 100644 index 0000000000..485b966a1e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntime.h @@ -0,0 +1,232 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace TestImpact +{ + class ChangeDependencyList; + class DynamicDependencyMap; + class TestSelectorAndPrioritizer; + class TestEngine; + class TestTarget; + class SourceCoveringTestsList; + class TestEngineInstrumentedRun; + + //! Callback for a test sequence that isn't using test impact analysis to determine selected tests. + //! @param tests The tests that will be run for this sequence. + using TestSequenceStartCallback = AZStd::function; + + //! Callback for a test sequence using test impact analysis. + //! @param selectedTests The tests that have been selected for this run by test impact analysis. + //! @param discardedTests The tests that have been rejected for this run by test impact analysis. + //! @param draftedTests The tests that have been drafted in for this run due to requirements outside of test impact analysis + //! (e.g. test targets that have been added to the repository since the last test impact analysis sequence or test that failed + //! to execute previously). + //! These tests will be run with coverage instrumentation. + //! @note discardedTests and draftedTests may contain overlapping tests. + using ImpactAnalysisTestSequenceStartCallback = AZStd::function&& discardedTests, + AZStd::vector&& draftedTests)>; + + //! Callback for a test sequence using test impact analysis. + //! @param selectedTests The tests that have been selected for this run by test impact analysis. + //! @param discardedTests The tests that have been rejected for this run by test impact analysis. + //! These tests will not be run without coverage instrumentation unless there is an entry in the draftedTests list. + //! @param draftedTests The tests that have been drafted in for this run due to requirements outside of test impact analysis + //! (e.g. test targets that have been added to the repository since the last test impact analysis sequence or test that failed + //! to execute previously). + //! @note discardedTests and draftedTests may contain overlapping tests. + using SafeImpactAnalysisTestSequenceStartCallback = AZStd::function&& draftedTests)>; + + //! Callback for end of a test sequence. + //! @param failureReport The test runs that failed for any reason during this sequence. + //! @param duration The total duration of this test sequence. + using TestSequenceCompleteCallback = AZStd::function; + + //! Callback for end of a test impact analysis test sequence. + //! @param selectedFailureReport The selected test runs that failed for any reason during this sequence. + //! @param discardedFailureReport The discarded test runs that failed for any reason during this sequence. + //! @param duration The total duration of this test sequence. + using SafeTestSequenceCompleteCallback = AZStd::function; + + //! Callback for test runs that have completed for any reason. + //! @param selectedTests The test that has completed. + using TestRunCompleteCallback = AZStd::function; + + //! The API exposed to the client responsible for all test runs and persistent data management. + class Runtime + { + public: + //! Constructs a runtime with the specified configuration and policies. + //! @param config The configuration used for this runtime instance. + //! @param suiteFilter The test suite for which the coverage data and test selection will draw from. + //! @param executionFailurePolicy Determines how to handle test targets that fail to execute. + //! @param executionFailureDraftingPolicy Determines how test targets that previously failed to execute are drafted into subsequent test sequences. + //! @param testFailurePolicy Determines how to handle test targets that report test failures. + //! @param integrationFailurePolicy Determines how to handle instances where the build system model and/or test impact analysis data is compromised. + //! @param testShardingPolicy Determines how to handle test targets that have opted in to test sharding. + Runtime( + RuntimeConfig&& config, + SuiteType suiteFilter, + Policy::ExecutionFailure executionFailurePolicy, + Policy::FailedTestCoverage failedTestCoveragePolicy, + Policy::TestFailure testFailurePolicy, + Policy::IntegrityFailure integrationFailurePolicy, + Policy::TestSharding testShardingPolicy, + Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional maxConcurrency = AZStd::nullopt); + + ~Runtime(); + + //! Runs a test sequence where all tests with a matching suite in the suite filter and also not on the excluded list are selected. + //! @param testTargetTimeout The maximum duration individual test targets may be in flight for (infinite if empty). + //! @param globalTimeout The maximum duration the entire test sequence may run for (infinite if empty). + //! @param testSequenceStartCallback The client function to be called after the test targets have been selected but prior to running the tests. + //! @param testSequenceCompleteCallback The client function to be called after the test sequence has completed. + //! @param testRunCompleteCallback The client function to be called after an individual test run has completed. + //! @returns + TestSequenceResult RegularTestSequence( + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceCompleteCallback, + AZStd::optional testRunCompleteCallback); + + //! Runs a test sequence where tests are selected according to test impact analysis so long as they are not on the excluded list. + //! @param changeList The change list used to determine the tests to select. + //! @param testPrioritizationPolicy Determines how selected tests will be prioritized. + //! @param dynamicDependencyMapPolicy The policy to determine how the coverage data of produced by test sequences is used to update the dynamic dependency map. + //! @param testTargetTimeout The maximum duration individual test targets may be in flight for (infinite if empty). + //! @param globalTimeout The maximum duration the entire test sequence may run for (infinite if empty). + //! @param testSequenceStartCallback The client function to be called after the test targets have been selected but prior to running the tests. + //! @param testSequenceCompleteCallback The client function to be called after the test sequence has completed. + //! @param testRunCompleteCallback The client function to be called after an individual test run has completed. + //! @returns + TestSequenceResult ImpactAnalysisTestSequence( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy, + Policy::DynamicDependencyMap dynamicDependencyMapPolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceCompleteCallback, + AZStd::optional testRunCompleteCallback); + + //! Runs a test sequence as per the ImpactAnalysisTestSequence where the tests not selected are also run (albeit without instrumentation). + //! @param changeList The change list used to determine the tests to select. + //! @param testPrioritizationPolicy Determines how selected tests will be prioritized. + //! @param testTargetTimeout The maximum duration individual test targets may be in flight for (infinite if empty). + //! @param globalTimeout The maximum duration the entire test sequence may run for (infinite if empty). + //! @param testSequenceStartCallback The client function to be called after the test targets have been selected but prior to running the tests. + //! @param testSequenceCompleteCallback The client function to be called after the test sequence has completed. + //! @param testRunCompleteCallback The client function to be called after an individual test run has completed. + //! @returns + AZStd::pair SafeImpactAnalysisTestSequence( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceCompleteCallback, + AZStd::optional testRunCompleteCallback); + + //! Runs all tests not on the excluded list and uses their coverage data to seed the test impact analysis data (ant existing data will be overwritten). + //! @param testTargetTimeout The maximum duration individual test targets may be in flight for (infinite if empty). + //! @param globalTimeout The maximum duration the entire test sequence may run for (infinite if empty). + //! @param testSequenceStartCallback The client function to be called after the test targets have been selected but prior to running the tests. + //! @param testSequenceCompleteCallback The client function to be called after the test sequence has completed. + //! @param testRunCompleteCallback The client function to be called after an individual test run has completed. + //! + TestSequenceResult SeededTestSequence( + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceCompleteCallback, + AZStd::optional testRunCompleteCallback); + + //! Returns true if the runtime has test impact analysis data (either preexisting or generated). + bool HasImpactAnalysisData() const; + + private: + //! Updates the test enumeration cache for test targets that had sources modified by a given change list. + //! @param changeDependencyList The resolved change dependency list generated for the change list. + void EnumerateMutatedTestTargets(const ChangeDependencyList& changeDependencyList); + + //! Selects the test targets covering a given change list and updates the enumeration cache of the test targets with sources + //! modified in that change list. + //! @param changeList The change list for which the covering tests and enumeration cache updates will be generated for. + //! @param testPrioritizationPolicy The test prioritization strategy to use for the selected test targets. + //! @returns The pair of selected test targets and discarded test targets. + AZStd::pair, AZStd::vector> SelectCoveringTestTargetsAndUpdateEnumerationCache( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy); + + //! Selects the test targets from the specified list of test targets that are not on the test target exclusion list. + //! @param testTargets The list of test targets to select from. + //! @returns The subset of test targets in the specified list that are not on the target exclude list. + AZStd::pair, AZStd::vector> SelectTestTargetsByExcludeList( + AZStd::vector testTargets) const; + + //! Prunes the existing coverage for the specified jobs and creates the consolidated source covering tests list from the + //! test engine instrumented run jobs. + SourceCoveringTestsList CreateSourceCoveringTestFromTestCoverages(const AZStd::vector& jobs); + + //! Prepares the dynamic dependency map for a seed update by clearing all existing data and deleting the file that will be serialized. + void ClearDynamicDependencyMapAndRemoveExistingFile(); + + //! Updates the dynamic dependency map and serializes the entire map to disk. + void UpdateAndSerializeDynamicDependencyMap(const AZStd::vector& jobs); + + RuntimeConfig m_config; + SuiteType m_suiteFilter; + RepoPath m_sparTIAFile; + Policy::ExecutionFailure m_executionFailurePolicy; + Policy::FailedTestCoverage m_failedTestCoveragePolicy; + Policy::TestFailure m_testFailurePolicy; + Policy::IntegrityFailure m_integrationFailurePolicy; + Policy::TestSharding m_testShardingPolicy; + Policy::TargetOutputCapture m_targetOutputCapture; + size_t m_maxConcurrency = 0; + AZStd::unique_ptr m_dynamicDependencyMap; + AZStd::unique_ptr m_testSelectorAndPrioritizer; + AZStd::unique_ptr m_testEngine; + AZStd::unordered_set m_testTargetExcludeList; + AZStd::unordered_set m_testTargetShardList; + bool m_hasImpactAnalysisData = false; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntimeException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntimeException.h new file mode 100644 index 0000000000..f178df685d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactRuntimeException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for runtime related exceptions. + class RuntimeException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactTestSequence.h b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactTestSequence.h new file mode 100644 index 0000000000..5a5a6196e2 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Include/TestImpactFramework/TestImpactTestSequence.h @@ -0,0 +1,127 @@ +/* + * 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 + +namespace TestImpact +{ + namespace Policy + { + //! Policy for handling of test targets that fail to execute (e.g. due to the binary not being found). + //! @note Test targets that fail to execute will be tagged such that their execution can be attempted at a later date. This is + //! important as otherwise it would be erroneously assumed that they cover no sources due to having no entries in the dynamic + //! dependency map. + enum class ExecutionFailure + { + Abort, //!< Abort the test sequence and report a failure. + Continue, //!< Continue the test sequence but treat the execution failures as test failures after the run. + Ignore //!< Continue the test sequence and ignore the execution failures. + }; + + //! Policy for handling the coverage data of failed tests targets (both test that failed to execute and tests that ran but failed). + enum class FailedTestCoverage + { + Discard, //!< Discard the coverage data produced by the failing tests, causing them to be drafted into future test runs. + Keep //!< Keep any existing coverage data and update the coverage data for failed test targetss that produce coverage. + }; + + //! Policy for prioritizing selected tests. + enum class TestPrioritization + { + None, //!< Do not attempt any test prioritization. + DependencyLocality //!< Prioritize test targets according to the locality of the production targets they cover in the build dependency graph. + }; + + //! Policy for handling test targets that report failing tests. + enum class TestFailure + { + Abort, //!< Abort the test sequence and report the test failure. + Continue //!< Continue the test sequence and report the test failures after the run. + }; + + //! Policy for handling integrity failures of the dynamic dependency map and the source to target mappings. + enum class IntegrityFailure + { + Abort, //!< Abort the test sequence and report the test failure. + Continue //!< Continue the test sequence and report the test failures after the run. + }; + + //! Policy for updating the dynamic dependency map with the coverage data of produced by test sequences. + enum class DynamicDependencyMap + { + Discard, //!< Discard the coverage data produced by test sequences. + Update //!< Update the dynamic dependency map with the coverage data produced by test sequences. + }; + + //! Policy for sharding test targets that have been marked for test sharding. + enum class TestSharding + { + Never, //!< Do not shard any test targets. + Always //!< Shard all test targets that have been marked for test sharding. + }; + + //! Standard output capture of test target runs. + enum class TargetOutputCapture + { + None, //!< Do not capture any output. + StdOut, //!< Send captured output to standard output + File, //!< Write captured output to file. + StdOutAndFile //!< Send captured output to standard output and write to file. + }; + } + + //! Configuration for test targets that opt in to test sharding. + enum class ShardConfiguration + { + Never, //!< Never shard this test target. + FixtureContiguous, //!< Each shard contains contiguous fixtures of tests (safest but least optimal). + TestContiguous, //!< Each shard contains contiguous tests agnostic of fixtures. + FixtureInterleaved, //!< Fixtures of tests are interleaved across shards. + TestInterleaved //!< Tests are interlaced across shards agnostic of fixtures (fastest but prone to inter-test dependency problems). + }; + + //! Test suite types to select from. + enum class SuiteType : AZ::u8 + { + Main = 0, + Periodic, + Sandbox + }; + + //! User-friendly names for the test suite types. + inline AZStd::string GetSuiteTypeName(SuiteType suiteType) + { + switch (suiteType) + { + case SuiteType::Main: + return "main"; + case SuiteType::Periodic: + return "periodic"; + case SuiteType::Sandbox: + return "sandbox"; + default: + throw(RuntimeException("Unexpected suite type")); + } + } + + //! Result of a test sequence that was run. + enum class TestSequenceResult + { + Success, //!< All tests ran with no failures. + Failure, //!< One or more tests failed and/or timed out and/or failed to launch and/or an integrity failure was encountered. + Timeout //!< The global timeout for the sequence was exceeded. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactCoverage.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactCoverage.h new file mode 100644 index 0000000000..4e1c435841 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactCoverage.h @@ -0,0 +1,42 @@ +/* + * 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 TestImpact +{ + //! Coverage information about a particular line. + struct LineCoverage + { + size_t m_lineNumber = 0; //!< The source line number this covers. + size_t m_hitCount = 0; //!< Number of times this line was covered (zero if not covered). + }; + + //! Coverage information about a particular source file. + struct SourceCoverage + { + RepoPath m_path; //!< Source file path. + AZStd::vector m_coverage; //!< Source file line coverage (empty if source level coverage only). + }; + + //! Coverage information about a particular module (executable, shared library). + struct ModuleCoverage + { + RepoPath m_path; //!< Module path. + AZStd::vector m_sources; //!< Sources of this module that are covered. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestEnumerationSuite.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestEnumerationSuite.h new file mode 100644 index 0000000000..e4c22a8787 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestEnumerationSuite.h @@ -0,0 +1,21 @@ +/* + * 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 TestImpact +{ + using TestEnumerationCase = TestCase; //!< Test case for test enumeration artifacts. + using TestEnumerationSuite = TestSuite; //!< Test suite for test enumeration artifacts. +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestRunSuite.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestRunSuite.h new file mode 100644 index 0000000000..22254db38e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestRunSuite.h @@ -0,0 +1,51 @@ +/* + * 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 TestImpact +{ + //! Result of a test that was ran. + enum class TestRunResult : bool + { + Failed, //! The given test failed. + Passed //! The given test passed. + }; + + //! Status of test as to whether or not it was ran. + enum class TestRunStatus : bool + { + NotRun, //!< The test was not run (typically because the test run was aborted by the client or runner before the test could run). + Run //!< The test was run (see TestRunResult for the result of this test). + }; + + //! Test case for test run artifacts. + struct TestRunCase + : public TestCase + { + AZStd::optional m_result; + AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds{0}; //! Duration this test took to run. + TestRunStatus m_status = TestRunStatus::NotRun; + }; + + //! Test suite for test run artifacts. + struct TestRunSuite + : public TestSuite + { + AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds{0}; //!< Duration this test suite took to run all of its tests. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestSuite.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestSuite.h new file mode 100644 index 0000000000..0646dd384a --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Dynamic/TestImpactTestSuite.h @@ -0,0 +1,35 @@ +/* + * 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 + +namespace TestImpact +{ + //! Artifact describing basic information about a test case. + struct TestCase + { + AZStd::string m_name; + bool m_enabled = false; + }; + + //! Artifact describing basic information about a test suite. + template + struct TestSuite + { + AZStd::string m_name; + bool m_enabled = false; + AZStd::vector m_tests; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.cpp new file mode 100644 index 0000000000..1bb88912d1 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.cpp @@ -0,0 +1,169 @@ +/* + * 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 + +namespace TestImpact +{ + AutogenSources PairAutogenSources( + const AZStd::vector& inputSources, + const AZStd::vector& outputSources, + const AZStd::string& autogenMatcher) + { + AutogenSources autogenSources; + const auto matcherPattern = AZStd::regex(autogenMatcher); + AZStd::smatch inputMatches, outputMatches; + + // This has the potential to be optimized to O(n(n-1)/2) time complexity but to be perfectly honest it's not a serious + // bottleneck right now and easier gains would be achieved by constructing build target artifacts in parallel rather than + // trying to squeeze any more juice here as each build target is independent of one and other with no shared memory + for (const auto& input : inputSources) + { + AutogenPairs autogenPairs; + autogenPairs.m_input = input.String(); + const AZStd::string inputString = input.Stem().Native(); + if (AZStd::regex_search(inputString, inputMatches, matcherPattern)) + { + for (const auto& output : outputSources) + { + const AZStd::string outputString = output.Stem().Native(); + if (AZStd::regex_search(outputString, outputMatches, matcherPattern)) + { + // Note: [0] contains the whole match, [1] contains the first capture group + const auto& inputMatch = inputMatches[1]; + const auto& outputMatch = outputMatches[1]; + if (inputMatch == outputMatch) + { + autogenPairs.m_outputs.emplace_back(output); + } + } + } + } + + if (!autogenPairs.m_outputs.empty()) + { + autogenSources.emplace_back(AZStd::move(autogenPairs)); + } + } + + return autogenSources; + } + + BuildTargetDescriptor BuildTargetDescriptorFactory( + const AZStd::string& buildTargetData, + const AZStd::vector& staticSourceExtensionIncludes, + const AZStd::vector& autogenInputExtensionIncludes, + const AZStd::string& autogenMatcher) + { + // Keys for pertinent JSON node and attribute names + constexpr const char* Keys[] = + { + "target", + "name", + "output_name", + "path", + "sources", + "static", + "input", + "output" + }; + + enum + { + TargetKey, + NameKey, + OutputNameKey, + PathKey, + SourcesKey, + StaticKey, + InputKey, + OutputKey + }; + + AZ_TestImpact_Eval(!autogenMatcher.empty(), ArtifactException, "Autogen matcher cannot be empty"); + + BuildTargetDescriptor buildTargetDescriptor; + rapidjson::Document buildTarget; + + if (buildTarget.Parse(buildTargetData.c_str()).HasParseError()) + { + throw TestImpact::ArtifactException("Could not parse build target data"); + } + + const auto& target = buildTarget[Keys[TargetKey]]; + buildTargetDescriptor.m_buildMetaData.m_name = target[Keys[NameKey]].GetString(); + buildTargetDescriptor.m_buildMetaData.m_outputName = target[Keys[OutputNameKey]].GetString(); + buildTargetDescriptor.m_buildMetaData.m_path = target["path"].GetString(); + + AZ_TestImpact_Eval(!buildTargetDescriptor.m_buildMetaData.m_name.empty(), ArtifactException, "Target name cannot be empty"); + AZ_TestImpact_Eval( + !buildTargetDescriptor.m_buildMetaData.m_outputName.empty(), ArtifactException, "Target output name cannot be empty"); + AZ_TestImpact_Eval(!buildTargetDescriptor.m_buildMetaData.m_path.empty(), ArtifactException, "Target path cannot be empty"); + + const auto& sources = buildTarget[Keys[SourcesKey]]; + const auto& staticSources = sources[Keys[StaticKey]].GetArray(); + if (!staticSources.Empty()) + { + buildTargetDescriptor.m_sources.m_staticSources = AZStd::vector(); + + for (const auto& source : staticSources) + { + const RepoPath sourcePath = RepoPath(source.GetString()); + if (AZStd::find( + staticSourceExtensionIncludes.begin(), staticSourceExtensionIncludes.end(), sourcePath.Extension().Native()) != + staticSourceExtensionIncludes.end()) + { + buildTargetDescriptor.m_sources.m_staticSources.emplace_back(AZStd::move(sourcePath)); + } + } + } + + const auto& inputSources = buildTarget[Keys[SourcesKey]][Keys[InputKey]].GetArray(); + const auto& outputSources = buildTarget[Keys[SourcesKey]][Keys[OutputKey]].GetArray(); + if (!inputSources.Empty() || !outputSources.Empty()) + { + AZ_TestImpact_Eval( + !inputSources.Empty() && !outputSources.Empty(), ArtifactException, "Autogen malformed, input or output sources are empty"); + + AZStd::vector inputPaths; + AZStd::vector outputPaths; + inputPaths.reserve(inputSources.Size()); + outputPaths.reserve(outputSources.Size()); + + for (const auto& source : inputSources) + { + const RepoPath sourcePath = RepoPath(source.GetString()); + if (AZStd::find( + autogenInputExtensionIncludes.begin(), autogenInputExtensionIncludes.end(), sourcePath.Extension().Native()) != + autogenInputExtensionIncludes.end()) + { + inputPaths.emplace_back(AZStd::move(sourcePath)); + } + } + + for (const auto& source : outputSources) + { + outputPaths.emplace_back(AZStd::move(RepoPath(source.GetString()))); + } + + buildTargetDescriptor.m_sources.m_autogenSources = PairAutogenSources(inputPaths, outputPaths, autogenMatcher); + } + + return buildTargetDescriptor; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.h new file mode 100644 index 0000000000..cf4e431a14 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.h @@ -0,0 +1,33 @@ +/* + * 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 TestImpact +{ + //! Constructs a build target artifact from the specified build target data. + //! @param buildTargetData The raw build target data in JSON format. + //! @param staticSourceIncludes The list of file extensions to include for static sources. + //! @param autogenInputExtentsionIncludes The list of file extensions to include for autogen input sources. + //! @param autogenMatcher The regex pattern used to match autogen input filenames with output filenames. + //! @return The constructed build target artifact. + BuildTargetDescriptor BuildTargetDescriptorFactory( + const AZStd::string& buildTargetData, + const AZStd::vector& staticSourceExtentsionIncludes, + const AZStd::vector& autogenInputExtentsionIncludes, + const AZStd::string& autogenMatcher); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.cpp new file mode 100644 index 0000000000..68eee409e7 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.cpp @@ -0,0 +1,144 @@ +/* + * 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 TestImpact +{ + namespace Cobertura + { + // Note: OpenCppCoverage appears to have a very liberal interpretation of the Cobertura coverage file format so consider + // this implementation to be provisional and coupled to the Windows platform and OpenCppCoverage tool + AZStd::vector ModuleCoveragesFactory(const AZStd::string& coverageData) + { + // Keys for pertinent XML node and attribute names + constexpr const char* Keys[] = + { + "packages", + "name", + "filename", + "coverage", + "classes", + "lines", + "line", + "number", + "hits", + "sources", + "source" + }; + + enum + { + PackagesKey, + NameKey, + FileNameKey, + CoverageKey, + ClassesKey, + LinesKey, + LineKey, + NumberKey, + HitsKey, + SourcesKey, + SourceKey + }; + + AZ_TestImpact_Eval(!coverageData.empty(), ArtifactException, "Cannot parse coverage, string is empty"); + AZStd::vector modules; + AZStd::vector rawData(coverageData.begin(), coverageData.end()); + + try + { + AZ::rapidxml::xml_document<> doc; + // Parse the XML doc with default flags + doc.parse<0>(rawData.data()); + + // Coverage + const auto coverage_node = doc.first_node(Keys[CoverageKey]); + AZ_TestImpact_Eval(coverage_node, ArtifactException, "Could not parse coverage node"); + + // Sources + const auto sources_node = coverage_node->first_node(Keys[SourcesKey]); + if (!sources_node) + { + return {}; + } + + // Source + const auto source_node = sources_node->first_node(Keys[SourceKey]); + if (!source_node) + { + return {}; + } + + // Root drive (this seems to be an unconventional use of the sources section by OpenCppCoverage) + const AZStd::string pathRoot = AZStd::string(source_node->value(), source_node->value() + source_node->value_size()) + "\\"; + + const auto packages_node = coverage_node->first_node(Keys[PackagesKey]); + if (packages_node) + { + // Modules + for (auto package_node = packages_node->first_node(); package_node; package_node = package_node->next_sibling()) + { + // Module + ModuleCoverage moduleCoverage; + moduleCoverage.m_path = package_node->first_attribute(Keys[NameKey])->value(); + + const auto classes_node = package_node->first_node(Keys[ClassesKey]); + if (classes_node) + { + // Sources + for (auto class_node = classes_node->first_node(); class_node; class_node = class_node->next_sibling()) + { + // Source + SourceCoverage sourceCoverage; + sourceCoverage.m_path = pathRoot + class_node->first_attribute(Keys[FileNameKey])->value(); + + const auto lines_node = class_node->first_node(Keys[LinesKey]); + if (lines_node) + { + // Lines + for (auto line_node = lines_node->first_node(); line_node; line_node = line_node->next_sibling()) + { + // Line + const size_t number = + AZStd::stol(AZStd::string(line_node->first_attribute(Keys[NumberKey])->value())); + const size_t hits = AZStd::stol(AZStd::string(line_node->first_attribute(Keys[HitsKey])->value())); + sourceCoverage.m_coverage.emplace_back(LineCoverage{number, hits}); + } + } + + moduleCoverage.m_sources.emplace_back(AZStd::move(sourceCoverage)); + } + } + + modules.emplace_back(AZStd::move(moduleCoverage)); + } + } + } + catch (const std::exception& e) + { + AZ_Error("ModuleCoveragesFactory", false, e.what()); + throw ArtifactException(e.what()); + } + catch (...) + { + throw ArtifactException("An unknown error occurred parsing the XML data"); + } + + return modules; + } + } // namespace Cobertura +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.h new file mode 100644 index 0000000000..c238901017 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactModuleCoverageFactory.h @@ -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 + +#include + +namespace TestImpact +{ + namespace Cobertura + { + //! Constructs a list of module coverage artifacts from the specified coverage data. + //! @param coverageData The raw coverage data in XML format. + //! @return The constructed list of module coverage artifacts. + AZStd::vector ModuleCoveragesFactory(const AZStd::string& coverageData); + } // namespace Cobertura +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.cpp new file mode 100644 index 0000000000..8162256aaf --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.cpp @@ -0,0 +1,91 @@ +/* + * 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 TestImpact +{ + namespace GTest + { + AZStd::vector TestEnumerationSuitesFactory(const AZStd::string& testEnumerationData) + { + // Keys for pertinent XML node and attribute names + constexpr const char* Keys[] = + { + "testsuites", + "testsuite", + "name", + "testcase" + }; + + enum + { + TestSuitesKey, + TestSuiteKey, + NameKey, + TestCaseKey + }; + + AZ_TestImpact_Eval(!testEnumerationData.empty(), ArtifactException, "Cannot parse enumeration, string is empty"); + AZStd::vector testSuites; + AZStd::vector rawData(testEnumerationData.begin(), testEnumerationData.end()); + + try + { + AZ::rapidxml::xml_document<> doc; + // Parse the XML doc with default flags + doc.parse<0>(rawData.data()); + + const auto testsuites_node = doc.first_node(Keys[TestSuitesKey]); + AZ_TestImpact_Eval(testsuites_node, ArtifactException, "Could not parse enumeration, XML is invalid"); + for (auto testsuite_node = testsuites_node->first_node(Keys[TestSuiteKey]); testsuite_node; + testsuite_node = testsuite_node->next_sibling()) + { + const auto isEnabled = [](const AZStd::string& name) + { + return !name.starts_with("DISABLED_") && name.find("/DISABLED_") == AZStd::string::npos; + }; + + TestEnumerationSuite testSuite; + testSuite.m_name = testsuite_node->first_attribute(Keys[NameKey])->value(); + testSuite.m_enabled = isEnabled(testSuite.m_name); + + for (auto testcase_node = testsuite_node->first_node(Keys[TestCaseKey]); testcase_node; + testcase_node = testcase_node->next_sibling()) + { + TestEnumerationCase testCase; + testCase.m_name = testcase_node->first_attribute(Keys[NameKey])->value(); + testCase.m_enabled = isEnabled(testCase.m_name); + testSuite.m_tests.emplace_back(AZStd::move(testCase)); + } + + testSuites.emplace_back(AZStd::move(testSuite)); + } + } + catch (const std::exception& e) + { + AZ_Error("TestEnumerationSuitesFactory", false, e.what()); + throw ArtifactException(e.what()); + } + catch (...) + { + throw ArtifactException("An unknown error occured parsing the XML data"); + } + + return testSuites; + } + } // namespace GTest +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.h new file mode 100644 index 0000000000..4be58b2116 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.h @@ -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 + +#include + +namespace TestImpact +{ + namespace GTest + { + //! Constructs a list of test enumeration suite artifacts from the specified test enumeraion data. + //! @param testEnumerationData The raw test enumeration data in XML format. + //! @return The constructed list of test enumeration suite artifacts. + AZStd::vector TestEnumerationSuitesFactory(const AZStd::string& testEnumerationData); + } // namespace GTest +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.cpp new file mode 100644 index 0000000000..ba5c398188 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.cpp @@ -0,0 +1,140 @@ +/* + * 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 + +namespace TestImpact +{ + namespace GTest + { + AZStd::vector TestRunSuitesFactory(const AZStd::string& testEnumerationData) + { + // Keys for pertinent XML node and attribute names + constexpr const char* Keys[] = + { + "testsuites", + "testsuite", + "name", + "testcase", + "status", + "run", + "notrun", + "time" + }; + + enum + { + TestSuitesKey, + TestSuiteKey, + NameKey, + TestCaseKey, + StatusKey, + RunKey, + NotRunKey, + DurationKey + }; + + AZ_TestImpact_Eval(!testEnumerationData.empty(), ArtifactException, "Cannot parse test run, string is empty"); + AZStd::vector testSuites; + AZStd::vector rawData(testEnumerationData.begin(), testEnumerationData.end()); + + try + { + AZ::rapidxml::xml_document<> doc; + // Parse the XML doc with default flags + doc.parse<0>(rawData.data()); + + const auto testsuites_node = doc.first_node(Keys[TestSuitesKey]); + AZ_TestImpact_Eval(testsuites_node, ArtifactException, "Could not parse enumeration, XML is invalid"); + for (auto testsuite_node = testsuites_node->first_node(Keys[TestSuiteKey]); testsuite_node; + testsuite_node = testsuite_node->next_sibling()) + { + const auto isEnabled = [](const AZStd::string& name) + { + return !name.starts_with("DISABLED_") && name.find("/DISABLED_") == AZStd::string::npos; + }; + + const auto getDuration = [&Keys](const AZ::rapidxml::xml_node<>* node) + { + const AZStd::string duration = node->first_attribute(Keys[DurationKey])->value(); + return AZStd::chrono::milliseconds(AZStd::stof(duration) * 1000.f); + }; + + TestRunSuite testSuite; + testSuite.m_name = testsuite_node->first_attribute(Keys[NameKey])->value(); + testSuite.m_enabled = isEnabled(testSuite.m_name); + testSuite.m_duration = getDuration(testsuite_node); + + for (auto testcase_node = testsuite_node->first_node(Keys[TestCaseKey]); testcase_node; + testcase_node = testcase_node->next_sibling()) + { + const auto getStatus = [&Keys](const AZ::rapidxml::xml_node<>* node) + { + const AZStd::string status = node->first_attribute(Keys[StatusKey])->value(); + if (status == Keys[RunKey]) + { + return TestRunStatus::Run; + } + else if (status == Keys[NotRunKey]) + { + return TestRunStatus::NotRun; + } + + throw ArtifactException(AZStd::string::format("Unexpected run status: %s", status.c_str())); + }; + + const auto getResult = [](const AZ::rapidxml::xml_node<>* node) + { + for (auto child_node = node->first_node("failure"); child_node; child_node = child_node->next_sibling()) + { + return TestRunResult::Failed; + } + + return TestRunResult::Passed; + }; + + TestRunCase testCase; + testCase.m_name = testcase_node->first_attribute(Keys[NameKey])->value(); + testCase.m_enabled = isEnabled(testCase.m_name); + testCase.m_duration = getDuration(testcase_node); + testCase.m_status = getStatus(testcase_node); + + if (testCase.m_status == TestRunStatus::Run) + { + testCase.m_result = getResult(testcase_node); + } + + testSuite.m_tests.emplace_back(AZStd::move(testCase)); + } + + testSuites.emplace_back(AZStd::move(testSuite)); + } + } + catch (const std::exception& e) + { + AZ_Error("TestRunSuitesFactory", false, e.what()); + throw ArtifactException(e.what()); + } + catch (...) + { + throw ArtifactException("An unknown error occurred parsing the XML data"); + } + + return testSuites; + } + } // namespace GTest +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.h new file mode 100644 index 0000000000..fb16c837b2 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestRunSuiteFactory.h @@ -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 + +#include + +namespace TestImpact +{ + namespace GTest + { + //! Constructs a list of test run suite artifacts from the specified test run data. + //! @param testRunData The raw test run data in XML format. + //! @return The constructed list of test run suite artifacts. + AZStd::vector TestRunSuitesFactory(const AZStd::string& testRunData); + } // namespace GTest +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaArtifactFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaArtifactFactory.h new file mode 100644 index 0000000000..f25af960b9 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaArtifactFactory.h @@ -0,0 +1,25 @@ +/* + * 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 + +namespace TestImpact +{ + //! Constructs a list of test target meta-data artifacts from the specified master test list data. + //! @param masterTestListData The raw master test list data in JSON format. + //! @return The constructed list of test target meta-data artifacts. + TestTargetMetas TestTargetMetaMapFactory(const AZStd::string& masterTestListData); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.cpp new file mode 100644 index 0000000000..4b247086ed --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.cpp @@ -0,0 +1,105 @@ +/* + * 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 TestImpact +{ + TestTargetMetaMap TestTargetMetaMapFactory(const AZStd::string& masterTestListData, SuiteType suiteType) + { + // Keys for pertinent JSON node and attribute names + constexpr const char* Keys[] = + { + "google", + "test", + "tests", + "suites", + "suite", + "launch_method", + "test_runner", + "stand_alone", + "name", + "command", + "timeout" + }; + + enum + { + GoogleKey, + TestKey, + TestsKey, + TestSuitesKey, + SuiteKey, + LaunchMethodKey, + TestRunnerKey, + StandAloneKey, + NameKey, + CommandKey, + TimeoutKey + }; + + AZ_TestImpact_Eval(!masterTestListData.empty(), ArtifactException, "test meta-data cannot be empty"); + + TestTargetMetaMap testMetas; + rapidjson::Document masterTestList; + + if (masterTestList.Parse(masterTestListData.c_str()).HasParseError()) + { + throw TestImpact::ArtifactException("Could not parse test meta-data"); + } + + const auto tests = masterTestList[Keys[GoogleKey]][Keys[TestKey]][Keys[TestsKey]].GetArray(); + for (const auto& test : tests) + { + TestTargetMeta testMeta; + const auto testSuites = test[Keys[TestSuitesKey]].GetArray(); + for (const auto& suite : testSuites) + { + // Check to see if this test target has the suite we're looking for + if (const auto suiteName = suite[Keys[SuiteKey]].GetString(); + strcmp(GetSuiteTypeName(suiteType).c_str(), suiteName) == 0) + { + testMeta.m_suite = suiteName; + testMeta.m_customArgs = suite[Keys[CommandKey]].GetString(); + testMeta.m_timeout = AZStd::chrono::seconds{ suite[Keys[TimeoutKey]].GetUint() }; + if (const auto buildTypeString = test[Keys[LaunchMethodKey]].GetString(); strcmp(buildTypeString, Keys[TestRunnerKey]) == 0) + { + testMeta.m_launchMethod = LaunchMethod::TestRunner; + } + else if (strcmp(buildTypeString, Keys[StandAloneKey]) == 0) + { + testMeta.m_launchMethod = LaunchMethod::StandAlone; + } + else + { + throw(ArtifactException("Unexpected test build type")); + } + + AZStd::string name = test[Keys[NameKey]].GetString(); + AZ_TestImpact_Eval(!name.empty(), ArtifactException, "Test name field cannot be empty"); + testMetas.emplace(AZStd::move(name), AZStd::move(testMeta)); + break; + } + } + } + + // If there's no tests in the repo then something is seriously wrong + AZ_TestImpact_Eval(!testMetas.empty(), ArtifactException, "No tests were found in the repository"); + + return testMetas; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.h new file mode 100644 index 0000000000..b08babf528 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.h @@ -0,0 +1,27 @@ +/* + * 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 TestImpact +{ + //! Constructs a list of test target meta-data artifacts of the specified suite type from the specified master test list data. + //! @param masterTestListData The raw master test list data in JSON format. + //! @param suiteType The suite type to select the target meta-data artifacts from. + //! @return The constructed list of test target meta-data artifacts. + TestTargetMetaMap TestTargetMetaMapFactory(const AZStd::string& masterTestListData, SuiteType suiteType); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.cpp new file mode 100644 index 0000000000..935ae14452 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.cpp @@ -0,0 +1,22 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + BuildTargetDescriptor::BuildTargetDescriptor(BuildMetaData&& buildMetaData, TargetSources&& sources) + : m_buildMetaData(AZStd::move(buildMetaData)) + , m_sources(AZStd::move(sources)) + { + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.h new file mode 100644 index 0000000000..49820a4bdb --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactBuildTargetDescriptor.h @@ -0,0 +1,56 @@ +/* + * 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 + +namespace TestImpact +{ + //! Pairing between a given autogen input source and the generated output source(s). + struct AutogenPairs + { + RepoPath m_input; + AZStd::vector m_outputs; + }; + + using AutogenSources = AZStd::vector; + + //! Representation of a given built target's source list. + struct TargetSources + { + AZStd::vector m_staticSources; //!< Source files used to build this target (if any). + AutogenSources m_autogenSources; //!< Autogen source files (if any). + }; + + //! Representation of a given build target's basic build infotmation. + struct BuildMetaData + { + AZStd::string m_name; //!< Build target name. + AZStd::string m_outputName; //!< Output name (sans extension) of build target binary. + RepoPath m_path; //!< Path to build target location in source tree (relative to repository root). + }; + + //! Artifact produced by the build system for each build target. Contains source and output information about said targets. + struct BuildTargetDescriptor + { + BuildTargetDescriptor() = default; + BuildTargetDescriptor(BuildMetaData&& buildMetaData, TargetSources&& sources); + + BuildMetaData m_buildMetaData; + TargetSources m_sources; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactDependencyGraphData.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactDependencyGraphData.h new file mode 100644 index 0000000000..3c9f455254 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactDependencyGraphData.h @@ -0,0 +1,27 @@ +/* + * 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 + +namespace TestImpact +{ + //! Raw representation of the dependency graph for a given build target. + struct DependencyGraphData + { + AZStd::string m_root; //!< The build target this dependency graph is for. + AZStd::vector m_vertices; //!< The depender/depending built targets in this graph. + AZStd::vector> m_edges; //!< The dependency connectivity of the build targets in this graph. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.cpp new file mode 100644 index 0000000000..1b9cb70dab --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.cpp @@ -0,0 +1,21 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + ProductionTargetDescriptor::ProductionTargetDescriptor(BuildTargetDescriptor&& buildTargetDescriptor) + : BuildTargetDescriptor(AZStd::move(buildTargetDescriptor)) + { + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.h new file mode 100644 index 0000000000..e9fccbc51a --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactProductionTargetDescriptor.h @@ -0,0 +1,25 @@ +/* + * 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 TestImpact +{ + //! Artifact produced by the target artifact compiler that represents a production build target in the repository. + struct ProductionTargetDescriptor + : public BuildTargetDescriptor + { + ProductionTargetDescriptor(BuildTargetDescriptor&& buildTarget); + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.cpp new file mode 100644 index 0000000000..e13b2083ae --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.cpp @@ -0,0 +1,45 @@ +/* + * 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 TestImpact +{ + AZStd::tuple, AZStd::vector> CompileTargetDescriptors( + AZStd::vector&& buildTargets, TestTargetMetaMap&& testTargetMetaMap) + { + AZ_TestImpact_Eval(!buildTargets.empty(), ArtifactException, "Build target descriptor list cannot be null"); + AZ_TestImpact_Eval(!testTargetMetaMap.empty(), ArtifactException, "Test target meta map cannot be null"); + + AZStd::tuple, AZStd::vector> outputTargets; + auto& [productionTargets, testTargets] = outputTargets; + + for (auto&& buildTarget : buildTargets) + { + // If this build target has an associated test artifact then it is a test target, otherwise it is a production target + if (auto&& testTargetMeta = testTargetMetaMap.find(buildTarget.m_buildMetaData.m_name); + testTargetMeta != testTargetMetaMap.end()) + { + testTargets.emplace_back(TestTargetDescriptor(AZStd::move(buildTarget), AZStd::move(testTargetMeta->second))); + } + else + { + productionTargets.emplace_back(ProductionTargetDescriptor(AZStd::move(buildTarget))); + } + } + + return outputTargets; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.h new file mode 100644 index 0000000000..608fa9d072 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTargetDescriptorCompiler.h @@ -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. + * + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace TestImpact +{ + //! Compiles the production target artifacts and test target artifactss from the supplied build target artifacts and test target meta + //! map artifact. + //! @param buildTargets The list of build target artifacts to be sorted into production and test artifact types. + //! @param testTargetMetaMap The map of test target meta artifacts containing the additional meta-data about each test target. + //! @return A tuple containing the production artifacts and test artifacts. + AZStd::tuple, AZStd::vector> CompileTargetDescriptors( + AZStd::vector&& buildTargets, TestTargetMetaMap&& testTargetMetaMap); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.cpp new file mode 100644 index 0000000000..f45e61c554 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.cpp @@ -0,0 +1,22 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + TestTargetDescriptor::TestTargetDescriptor(BuildTargetDescriptor&& buildTarget, TestTargetMeta&& testTargetMeta) + : BuildTargetDescriptor(AZStd::move(buildTarget)) + , m_testMetaData(AZStd::move(testTargetMeta)) + { + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.h new file mode 100644 index 0000000000..7044c963e8 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetDescriptor.h @@ -0,0 +1,28 @@ +/* + * 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 + +namespace TestImpact +{ + //! Artifact produced by the target artifact compiler that represents a test build target in the repository. + struct TestTargetDescriptor + : public BuildTargetDescriptor + { + TestTargetDescriptor(BuildTargetDescriptor&& buildTarget, TestTargetMeta&& testTargetMeta); + + TestTargetMeta m_testMetaData; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetMeta.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetMeta.h new file mode 100644 index 0000000000..bef9d9a44f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/Static/TestImpactTestTargetMeta.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 +#include +#include + +namespace TestImpact +{ + //! Method used to launch the test target. + enum class LaunchMethod : bool + { + TestRunner, //!< Target is launched through a separate test runner binary. + StandAlone //!< Target is launched directly by itself. + }; + + //! Artifact produced by the build system for each test target containing the additional meta-data about the test. + struct TestTargetMeta + { + AZStd::string m_suite; + AZStd::string m_customArgs; + AZStd::chrono::milliseconds m_timeout = AZStd::chrono::milliseconds{ 0 }; + LaunchMethod m_launchMethod = LaunchMethod::TestRunner; + }; + + //! Map between test target name and test target meta-data. + using TestTargetMetaMap = AZStd::unordered_map; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/TestImpactArtifactException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/TestImpactArtifactException.h new file mode 100644 index 0000000000..5d2a4fabd2 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Artifact/TestImpactArtifactException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for artifacts and artifact parsing operations. + class ArtifactException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.cpp new file mode 100644 index 0000000000..53e71d3a30 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.cpp @@ -0,0 +1,41 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + ChangeDependencyList::ChangeDependencyList( + AZStd::vector&& createSourceDependencies, + AZStd::vector&& updateSourceDependencies, + AZStd::vector&& deleteSourceDependencies) + : m_createSourceDependencies(AZStd::move(createSourceDependencies)) + , m_updateSourceDependencies(AZStd::move(updateSourceDependencies)) + , m_deleteSourceDependencies(AZStd::move(deleteSourceDependencies)) + { + } + + const AZStd::vector& ChangeDependencyList::GetCreateSourceDependencies() const + { + return m_createSourceDependencies; + } + + const AZStd::vector& ChangeDependencyList::GetUpdateSourceDependencies() const + { + return m_updateSourceDependencies; + } + + const AZStd::vector& ChangeDependencyList::GetDeleteSourceDependencies() const + { + return m_deleteSourceDependencies; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.h new file mode 100644 index 0000000000..bf8b0f558d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactChangeDependencyList.h @@ -0,0 +1,41 @@ +/* + * 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 TestImpact +{ + //! Representation of a change list where all CRUD sources have been resolved to source dependencies from the dynamic dependency map. + class ChangeDependencyList + { + public: + ChangeDependencyList( + AZStd::vector&& createSourceDependencies, + AZStd::vector&& updateSourceDependencies, + AZStd::vector&& deleteSourceDependencies); + + //! Gets the sources dependencies of the created source files from the change list. + const AZStd::vector& GetCreateSourceDependencies() const; + + //! Gets the sources dependencies of the updated source files from the change list. + const AZStd::vector& GetUpdateSourceDependencies() const; + + //! Gets the sources dependencies of the deleted source files from the change list. + const AZStd::vector& GetDeleteSourceDependencies() const; + private: + AZStd::vector m_createSourceDependencies; + AZStd::vector m_updateSourceDependencies; + AZStd::vector m_deleteSourceDependencies; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDependencyException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDependencyException.h new file mode 100644 index 0000000000..e256fa3ccc --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDependencyException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for dependency related operations. + class DependencyException + : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.cpp new file mode 100644 index 0000000000..28b817e32c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.cpp @@ -0,0 +1,507 @@ +/* + * 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 + +namespace TestImpact +{ + DynamicDependencyMap::DynamicDependencyMap( + AZStd::vector&& productionTargetDescriptors, + AZStd::vector&& testTargetDescriptors) + : m_productionTargets(AZStd::move(productionTargetDescriptors)) + , m_testTargets(AZStd::move(testTargetDescriptors)) + { + const auto mapBuildTargetSources = [this](const auto* target) + { + for (const auto& source : target->GetSources().m_staticSources) + { + if (auto mapping = m_sourceDependencyMap.find(source.String()); + mapping != m_sourceDependencyMap.end()) + { + // This is an existing entry in the dependency map so update the parent build targets with this target + mapping->second.m_parentTargets.insert(target); + } + else + { + // This is a new entry on the dependency map so create an entry with this parent target and no covering targets + m_sourceDependencyMap.emplace(source.String(), DependencyData{ {target}, {} }); + } + } + + // Populate the autogen input to output mapping with any autogen sources + for (const auto& autogen : target->GetSources().m_autogenSources) + { + for (const auto& output : autogen.m_outputs) + { + m_autogenInputToOutputMap[autogen.m_input.String()].push_back(output.String()); + } + } + }; + + for (const auto& target : m_productionTargets.GetTargets()) + { + mapBuildTargetSources(&target); + } + + for (const auto& target : m_testTargets.GetTargets()) + { + mapBuildTargetSources(&target); + m_testTargetSourceCoverage[&target] = {}; + } + } + + size_t DynamicDependencyMap::GetNumTargets() const + { + return m_productionTargets.GetNumTargets() + m_testTargets.GetNumTargets(); + } + + size_t DynamicDependencyMap::GetNumSources() const + { + return m_sourceDependencyMap.size(); + } + + const BuildTarget* DynamicDependencyMap::GetBuildTarget(const AZStd::string& name) const + { + const BuildTarget* buildTarget = nullptr; + AZStd::visit([&buildTarget](auto&& target) + { + if constexpr (IsProductionTarget || IsTestTarget) + { + buildTarget = target; + } + + }, GetTarget(name)); + + return buildTarget; + } + + const BuildTarget* DynamicDependencyMap::GetBuildTargetOrThrow(const AZStd::string& name) const + { + const BuildTarget* buildTarget = nullptr; + AZStd::visit([&buildTarget](auto&& target) + { + if constexpr (IsProductionTarget || IsTestTarget) + { + buildTarget = target; + } + }, GetTargetOrThrow(name)); + + return buildTarget; + } + + OptionalTarget DynamicDependencyMap::GetTarget(const AZStd::string& name) const + { + if (const auto testTarget = m_testTargets.GetTarget(name); + testTarget != nullptr) + { + return testTarget; + } + else if (auto productionTarget = m_productionTargets.GetTarget(name); + productionTarget != nullptr) + { + return productionTarget; + } + + return AZStd::monostate{}; + } + + Target DynamicDependencyMap::GetTargetOrThrow(const AZStd::string& name) const + { + Target buildTarget; + AZStd::visit([&buildTarget, &name](auto&& target) + { + if constexpr (IsProductionTarget || IsTestTarget) + { + buildTarget = target; + } + else + { + throw(TargetException(AZStd::string::format("Couldn't find target %s", name.c_str()).c_str())); + } + }, GetTarget(name)); + + return buildTarget; + } + + void DynamicDependencyMap::ReplaceSourceCoverageInternal(const SourceCoveringTestsList& sourceCoverageDelta, bool pruneIfNoParentsOrCoverage) + { + AZStd::vector killList; + for (const auto& sourceCoverage : sourceCoverageDelta.GetCoverage()) + { + // Autogen input files are not compiled sources and thus supplying coverage data for them makes no sense + AZ_TestImpact_Eval( + m_autogenInputToOutputMap.find(sourceCoverage.GetPath().String()) == m_autogenInputToOutputMap.end(), + DependencyException, AZStd::string::format("Couldn't replace source coverage for %s, source file is an autogen input file", + sourceCoverage.GetPath().c_str()).c_str()); + + auto [sourceDependencyIt, inserted] = m_sourceDependencyMap.insert(sourceCoverage.GetPath().String()); + auto& [source, sourceDependency] = *sourceDependencyIt; + + // Before we can replace the coverage for this source dependency, we must: + // 1. Remove the source from the test target covering sources map + // 2. Prune the covered targets for the parent test target(s) of this source dependency + // 3. Clear any existing coverage for the delta + + // 1. + for (const auto& testTarget : sourceDependency.m_coveringTestTargets) + { + if (auto coveringTestTargetIt = m_testTargetSourceCoverage.find(testTarget); + coveringTestTargetIt != m_testTargetSourceCoverage.end()) + { + coveringTestTargetIt->second.erase(source); + } + + + } + + // 2. + // This step is prohibitively expensive as it requires iterating over all of the sources of the build targets covered by + // the parent test targets of this source dependency to ensure that this is in fact the last source being cleared and thus + // it can be determined that the parent test target is no longer covering the given build target + // + // The implications of this are that multiple calls to the test selector and priritizor's SelectTestTargets method will end + // up pulling in more test targets than needed for newly-created production sources until the next time the dynamic dependency + // map reconstructed, however until this use case materializes the implications described will not be addressed + + // 3. + sourceDependency.m_coveringTestTargets.clear(); + + // Update the dependency with any new coverage data + for (const auto& unresolvedTestTarget : sourceCoverage.GetCoveringTestTargets()) + { + if (const TestTarget* testTarget = m_testTargets.GetTarget(unresolvedTestTarget); + testTarget) + { + // Source to covering test target mapping + sourceDependency.m_coveringTestTargets.insert(testTarget); + + // Add the source to the test target covering sources map + m_testTargetSourceCoverage[testTarget].insert(source); + + // Build target to covering test target mapping + for (const auto& parentTarget : sourceDependency.m_parentTargets) + { + m_buildTargetCoverage[parentTarget.GetBuildTarget()].insert(testTarget); + } + } + else + { + AZ_Warning("ReplaceSourceCoverage", false, AZStd::string::format("Test target %s exists in the coverage data " + "but has since been removed from the build system", unresolvedTestTarget.c_str()).c_str()); + } + } + + // If the new coverage data results in a parentless and coverageless entry, consider it a dead entry and remove accordingly + if (sourceDependency.m_coveringTestTargets.empty() && sourceDependency.m_parentTargets.empty() && pruneIfNoParentsOrCoverage) + { + m_sourceDependencyMap.erase(sourceDependencyIt); + } + } + } + + void DynamicDependencyMap::ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta) + { + ReplaceSourceCoverageInternal(sourceCoverageDelta, true); + } + + void DynamicDependencyMap::ClearSourceCoverage(const AZStd::vector& paths) + { + for (const auto& path : paths) + { + if (const auto outputSources = m_autogenInputToOutputMap.find(path.String()); + outputSources != m_autogenInputToOutputMap.end()) + { + // Clearing the coverage data of an autogen input source instead clears the coverage data of its output sources + for (const auto& outputSource : outputSources->second) + { + ReplaceSourceCoverage(SourceCoveringTestsList(AZStd::vector{ SourceCoveringTests(RepoPath(outputSource)) })); + } + } + else + { + ReplaceSourceCoverage(SourceCoveringTestsList(AZStd::vector{ SourceCoveringTests(RepoPath(path)) })); + } + } + } + + void DynamicDependencyMap::ClearAllSourceCoverage() + { + for (auto it = m_sourceDependencyMap.begin(); it != m_sourceDependencyMap.end(); ++it) + { + const auto& [path, coverage] = *it; + ReplaceSourceCoverageInternal(SourceCoveringTestsList(AZStd::vector{ SourceCoveringTests(RepoPath(path)) }), false); + if (coverage.m_coveringTestTargets.empty() && coverage.m_parentTargets.empty()) + { + it = m_sourceDependencyMap.erase(it); + } + } + } + + const ProductionTargetList& DynamicDependencyMap::GetProductionTargetList() const + { + return m_productionTargets; + } + + const TestTargetList& DynamicDependencyMap::GetTestTargetList() const + { + return m_testTargets; + } + + AZStd::vector DynamicDependencyMap::GetCoveringTestTargetsForProductionTarget(const ProductionTarget& productionTarget) const + { + AZStd::vector coveringTestTargets; + if (const auto coverage = m_buildTargetCoverage.find(&productionTarget); + coverage != m_buildTargetCoverage.end()) + { + coveringTestTargets.reserve(coverage->second.size()); + AZStd::copy(coverage->second.begin(), coverage->second.end(), AZStd::back_inserter(coveringTestTargets)); + } + + return coveringTestTargets; + } + + AZStd::optional DynamicDependencyMap::GetSourceDependency(const RepoPath& path) const + { + AZStd::unordered_set parentTargets; + AZStd::unordered_set coveringTestTargets; + + const auto getSourceDependency = [&parentTargets, &coveringTestTargets, this](const AZStd::string& path) + { + const auto sourceDependency = m_sourceDependencyMap.find(path); + if (sourceDependency != m_sourceDependencyMap.end()) + { + for (const auto& parentTarget : sourceDependency->second.m_parentTargets) + { + parentTargets.insert(parentTarget); + } + + for (const auto& testTarget : sourceDependency->second.m_coveringTestTargets) + { + coveringTestTargets.insert(testTarget); + } + } + }; + + if (const auto outputSources = m_autogenInputToOutputMap.find(path.String()); outputSources != m_autogenInputToOutputMap.end()) + { + // Consolidate the parentage and coverage of each of the autogen input file's generated output files + for (const auto& outputSource : outputSources->second) + { + getSourceDependency(outputSource); + } + } + else + { + getSourceDependency(path.String()); + } + + if (!parentTargets.empty() || !coveringTestTargets.empty()) + { + return SourceDependency(path, DependencyData{ AZStd::move(parentTargets), AZStd::move(coveringTestTargets) }); + } + + return AZStd::nullopt; + } + + SourceDependency DynamicDependencyMap::GetSourceDependencyOrThrow(const RepoPath& path) const + { + auto sourceDependency = GetSourceDependency(path); + AZ_TestImpact_Eval(sourceDependency.has_value(), DependencyException, AZStd::string::format("Couldn't find source %s", path.c_str()).c_str()); + return sourceDependency.value(); + } + + SourceCoveringTestsList DynamicDependencyMap::ExportSourceCoverage() const + { + AZStd::vector coverage; + for (const auto& [path, dependency] : m_sourceDependencyMap) + { + AZStd::vector souceCoveringTests; + for (const auto& testTarget : dependency.m_coveringTestTargets) + { + souceCoveringTests.push_back(testTarget->GetName()); + } + + coverage.push_back(SourceCoveringTests(RepoPath(path), AZStd::move(souceCoveringTests))); + } + + return SourceCoveringTestsList(AZStd::move(coverage)); + } + + AZStd::vector DynamicDependencyMap::GetOrphanSourceFiles() const + { + AZStd::vector orphans; + for (const auto& [source, dependency] : m_sourceDependencyMap) + { + if (dependency.m_parentTargets.empty()) + { + orphans.push_back(source); + } + } + + return orphans; + } + + ChangeDependencyList DynamicDependencyMap::ApplyAndResoveChangeList(const ChangeList& changeList) + { + AZStd::vector createDependencies; + AZStd::vector updateDependencies; + AZStd::vector deleteDependencies; + + // Keep track of the coverage to delete as a post step rather than deleting it in situ so that erroneous change lists + // do not corrupt the dynamic dependency map + AZStd::vector coverageToDelete; + + // Create operations + for (const auto& createdFile : changeList.m_createdFiles) + { + auto sourceDependency = GetSourceDependency(createdFile); + if (sourceDependency.has_value()) + { + if (sourceDependency->GetNumCoveringTestTargets()) + { + const AZStd::string msg = AZStd::string::format("The newly-created file %s belongs to a build target yet " + "still has coverage data in the source covering test list implying that a delete CRUD operation has been " + "missed, thus the integrity of the source covering test list has been compromised", createdFile.c_str()); + AZ_Error("File Creation", false, msg.c_str()); + throw DependencyException(msg); + } + + if (sourceDependency->GetNumParentTargets()) + { + createDependencies.emplace_back(AZStd::move(*sourceDependency)); + } + } + } + + // Update operations + for (const auto& updatedFile : changeList.m_updatedFiles) + { + auto sourceDependency = GetSourceDependency(updatedFile); + if (sourceDependency.has_value()) + { + if (sourceDependency->GetNumParentTargets()) + { + updateDependencies.emplace_back(AZStd::move(*sourceDependency)); + } + else + { + if (sourceDependency->GetNumCoveringTestTargets()) + { + AZ_Printf( + "File Update", AZStd::string::format("Source file '%s' is potentially an orphan (used by build targets " + "without explicitly being added to the build system, e.g. an include directive pulling in a header from the " + "repository). Running the covering tests for this file with instrumentation will confirm whether or nor this " + "is the case.\n", updatedFile.c_str()).c_str()); + + updateDependencies.emplace_back(AZStd::move(*sourceDependency)); + coverageToDelete.push_back(updatedFile); + } + } + } + } + + // Delete operations + for (const auto& deletedFile : changeList.m_deletedFiles) + { + auto sourceDependency = GetSourceDependency(deletedFile); + if (!sourceDependency.has_value()) + { + continue; + } + + if (sourceDependency->GetNumParentTargets()) + { + if (sourceDependency->GetNumCoveringTestTargets()) + { + const AZStd::string msg = AZStd::string::format("The deleted file %s still belongs to a build target and still " + "has coverage data in the source covering test list, implying that the integrity of both the source to target " + "mappings and the source covering test list has been compromised", deletedFile.c_str()); + AZ_Error("File Delete", false, msg.c_str()); + throw DependencyException(msg); + } + else + { + const AZStd::string msg = AZStd::string::format("The deleted file %s still belongs to a build target implying " + "that the integrity of the source to target mappings has been compromised", deletedFile.c_str()); + AZ_Error("File Delete", false, msg.c_str()); + throw DependencyException(msg); + } + } + else + { + if (sourceDependency->GetNumCoveringTestTargets()) + { + deleteDependencies.emplace_back(AZStd::move(*sourceDependency)); + coverageToDelete.push_back(deletedFile); + } + } + } + + if (!coverageToDelete.empty()) + { + ClearSourceCoverage(coverageToDelete); + } + + return ChangeDependencyList(AZStd::move(createDependencies), AZStd::move(updateDependencies), AZStd::move(deleteDependencies)); + } + + void DynamicDependencyMap::RemoveTestTargetFromSourceCoverage(const TestTarget* testTarget) + { + if (const auto& it = m_testTargetSourceCoverage.find(testTarget); + it != m_testTargetSourceCoverage.end()) + { + for (const auto& source : it->second) + { + const auto sourceDependency = m_sourceDependencyMap.find(source); + AZ_TestImpact_Eval( + sourceDependency != m_sourceDependencyMap.end(), + DependencyException, + AZStd::string::format("Test target '%s' has covering source '%s' yet cannot be found in the dependency map", + testTarget->GetName().c_str(), source.c_str())); + + sourceDependency->second.m_coveringTestTargets.erase(testTarget); + } + + m_testTargetSourceCoverage.erase(testTarget); + } + } + + AZStd::vector DynamicDependencyMap::GetCoveringTests() const + { + AZStd::vector covering; + for (const auto& [testTarget, coveringSources] : m_testTargetSourceCoverage) + { + if (!coveringSources.empty()) + { + covering.push_back(testTarget); + } + } + + return covering; + } + + AZStd::vector DynamicDependencyMap::GetNotCoveringTests() const + { + AZStd::vector notCovering; + for(const auto& [testTarget, coveringSources] : m_testTargetSourceCoverage) + { + if (coveringSources.empty()) + { + notCovering.push_back(testTarget); + } + } + + return notCovering; + } + +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.h new file mode 100644 index 0000000000..701f69e68d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactDynamicDependencyMap.h @@ -0,0 +1,142 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace TestImpact +{ + //! Representation of the repository source tree and its relation to the build targets and coverage data. + class DynamicDependencyMap + { + public: + //! Constructs the dependency map with entries for each build target's source files with empty test coverage data. + DynamicDependencyMap( + AZStd::vector&& productionTargetDescriptors, + AZStd::vector&& testTargetDescriptors); + + //! Gets the total number of production and test targets in the repository. + size_t GetNumTargets() const; + + //! Gets the total number of unique source files in the repository. + //! @note This includes autogen output sources. + size_t GetNumSources() const; + + //! Attempts to get the specified build target. + //! @param name The name of the build target to get. + //! @returns If found, the pointer to the specified build target, otherwise nullptr. + const BuildTarget* GetBuildTarget(const AZStd::string& name) const; + + //! Attempts to get the specified build target or throw TargetException. + //! @param name The name of the build target to get. + const BuildTarget* GetBuildTargetOrThrow(const AZStd::string& name) const; + + //! Attempts to get the specified target's specialized type. + //! @param name The name of the target to get. + //! @returns If found, the pointer to the specialized target, otherwise AZStd::monostate. + OptionalTarget GetTarget(const AZStd::string& name) const; + + //! Attempts to get the specified target's specialized type or throw TargetException. + //! @param name The name of the target to get. + Target GetTargetOrThrow(const AZStd::string& name) const; + + //! Get the list of production targets in the repository. + const ProductionTargetList& GetProductionTargetList() const; + + //! Get the list of test targets in the repository. + const TestTargetList& GetTestTargetList() const; + + //! Gets the test targets covering the specified production target. + //! @param productionTarget The production target to retrieve the covering tests for. + AZStd::vector GetCoveringTestTargetsForProductionTarget(const ProductionTarget& productionTarget) const; + + //! Gets the source dependency for the specified source file. + //! @note Autogen input source dependencies are the consolidated source dependencies of all of their generated output sources. + //! @returns If found, the source dependency information for the specified source file, otherwise empty. + AZStd::optional GetSourceDependency(const RepoPath& path) const; + + //! Gets the source dependency for the specified source file or throw DependencyException. + SourceDependency GetSourceDependencyOrThrow(const RepoPath& path) const; + + //! Replaces the source coverage of the specified sources with the specified source coverage. + //! @param sourceCoverageDelta The source coverage delta to replace in the dependency map. + void ReplaceSourceCoverage(const SourceCoveringTestsList& sourceCoverageDelta); + + //! Clears all of the existing source coverage in the dependency map. + void ClearAllSourceCoverage(); + + //! Exports the coverage of all sources in the dependency map. + SourceCoveringTestsList ExportSourceCoverage() const; + + //! Gets the list of orphaned source files in the dependency map that have coverage data but belong to no parent build targets. + AZStd::vector GetOrphanSourceFiles() const; + + //! Applies the specified change list to the dependency map and resolves the change list to a change dependency list + //! containing the updated source dependencies for each source file in the change list. + //! @param changeList The change list to apply and resolve. + //! @returns The change list as resolved to the appropriate source dependencies. + [[nodiscard]] ChangeDependencyList ApplyAndResoveChangeList(const ChangeList& changeList); + + //! Removes the specified test target from all source coverage. + void RemoveTestTargetFromSourceCoverage(const TestTarget* testTarget); + + //! Returns the test targets that cover one or more sources in the repository. + AZStd::vector GetCoveringTests() const; + + //! Returns the test targets that do not cover any sources in the repository. + AZStd::vector GetNotCoveringTests() const; + + private: + //! Internal handler for ReplaceSourceCoverage where the pruning of parentless and coverageless source depenencies after the + //! source coverage has been replaced must be explicitly stated. + //! @note The covered targets for the source dependency's parent test target(s) will not be pruned if those covering targets are removed. + //! @param sourceCoverageDelta The source coverage delta to replace in the dependency map. + //! @param pruneIfNoParentsOrCoverage Flag to specify whether or not newly parentless and coverageless dependencies will be removed. + void ReplaceSourceCoverageInternal(const SourceCoveringTestsList& sourceCoverageDelta, bool pruneIfNoParentsOrCoverage); + + //! Clears the source coverage of the specified sources. + //! @note The covering targets for the parent test target(s) will not be pruned if those covering targets are removed. + void ClearSourceCoverage(const AZStd::vector& paths); + + //! The sorted list of unique production targets in the repository. + ProductionTargetList m_productionTargets; + + //! The sorted list of unique test targets in the repository. + TestTargetList m_testTargets; + + //! The dependency map of sources to their parent build targets and covering test targets. + AZStd::unordered_map m_sourceDependencyMap; + + //! Map of all test targets and the sources they cover. + AZStd::unordered_map> m_testTargetSourceCoverage; + + //! The map of build targets and their covering test targets. + //! @note As per the note for ReplaceSourceCoverageInternal, this map is currently not pruned when source coverage is replaced. + AZStd::unordered_map> m_buildTargetCoverage; + + //! Mapping of autogen input sources to their generated output sources. + AZStd::unordered_map> m_autogenInputToOutputMap; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.cpp new file mode 100644 index 0000000000..ce8bf64678 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.cpp @@ -0,0 +1,81 @@ +/* + * 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 + +namespace TestImpact +{ + AZStd::vector ExtractTargetsFromSet(AZStd::unordered_set&& coveringTestTargets) + { + AZStd::vector testTargets; + testTargets.reserve(coveringTestTargets.size()); + for (auto it = coveringTestTargets.begin(); it != coveringTestTargets.end(); ) + { + testTargets.push_back(std::move(coveringTestTargets.extract(it++).value())); + } + + return testTargets; + } + + SourceCoveringTests::SourceCoveringTests(const RepoPath& path) + : m_path(path) + { + } + + SourceCoveringTests::SourceCoveringTests(const RepoPath& path, AZStd::vector&& coveringTestTargets) + : m_path(path) + , m_coveringTestTargets(AZStd::move(coveringTestTargets)) + { + } + + SourceCoveringTests::SourceCoveringTests(const RepoPath& path, AZStd::unordered_set&& coveringTestTargets) + : m_path(path) + , m_coveringTestTargets(ExtractTargetsFromSet(AZStd::move(coveringTestTargets))) + { + } + + const RepoPath& SourceCoveringTests::GetPath() const + { + return m_path; + } + + size_t SourceCoveringTests::GetNumCoveringTestTargets() const + { + return m_coveringTestTargets.size(); + } + + const AZStd::vector& SourceCoveringTests::GetCoveringTestTargets() const + { + return m_coveringTestTargets; + } + + SourceCoveringTestsList::SourceCoveringTestsList(AZStd::vector&& sourceCoveringTests) + : m_coverage(AZStd::move(sourceCoveringTests)) + { + AZStd::sort(m_coverage.begin(), m_coverage.end(), [](const SourceCoveringTests& lhs, const SourceCoveringTests& rhs) + { + return lhs.GetPath().String() < rhs.GetPath().String(); + }); + } + + size_t SourceCoveringTestsList::GetNumSources() const + { + return m_coverage.size(); + } + + const AZStd::vector& SourceCoveringTestsList::GetCoverage() const + { + return m_coverage; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.h new file mode 100644 index 0000000000..f501ccefd8 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsList.h @@ -0,0 +1,59 @@ +/* + * 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 + +namespace TestImpact +{ + //! Represents the unresolved test target coverage for a given source file. + class SourceCoveringTests + { + public: + //SourceCoveringTests(const SourceCoveringTests&); + explicit SourceCoveringTests(const RepoPath& path); + SourceCoveringTests(const RepoPath& path, AZStd::vector&& coveringTestTargets); + SourceCoveringTests(const RepoPath& path, AZStd::unordered_set&& coveringTestTargets); + + //! Returns the path of this source file. + const RepoPath& GetPath() const; + + //! Returns the number of unresolved test targets covering this source file. + size_t GetNumCoveringTestTargets() const; + + //! Returns the unresolved test targets covering this source file. + const AZStd::vector& GetCoveringTestTargets() const; + private: + RepoPath m_path; //!< The path of this source file. + AZStd::vector m_coveringTestTargets; //!< The unresolved test targets that cover this source file. + }; + + //! Sorted collection of source file test coverage. + class SourceCoveringTestsList + { + public: + explicit SourceCoveringTestsList(AZStd::vector&& sourceCoveringTests); + + //! Returns the number of source files in the collection. + size_t GetNumSources() const; + + //! Returns the source file coverages. + const AZStd::vector& GetCoverage() const; + private: + AZStd::vector m_coverage; //!< The collection of source file coverages. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.cpp new file mode 100644 index 0000000000..5143f9b73c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.cpp @@ -0,0 +1,93 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace TestImpact +{ + // Tag used to indicate whether a given line is the name or a covering test target + constexpr char TargetTag = '-'; + + AZStd::string SerializeSourceCoveringTestsList(const SourceCoveringTestsList& sourceCoveringTestsList) + { + AZStd::string output; + output.reserve(1U << 24); // Reserve approx. 16Mib as the outputs can be quite large + + for (const auto& source : sourceCoveringTestsList.GetCoverage()) + { + // Source file + output += source.GetPath().String(); + output += "\n"; + + // Covering test targets + for (const auto& testTarget : source.GetCoveringTestTargets()) + { + output += AZStd::string::format("%c%s\n", TargetTag, testTarget.c_str()); + } + } + + // Add the newline so the deserializer can properly terminate on the last read line + output += "\n"; + + return output; + } + + SourceCoveringTestsList DeserializeSourceCoveringTestsList(const AZStd::string& sourceCoveringTestsListString) + { + AZStd::vector sourceCoveringTests; + AZStd::string source; + AZStd::vector coveringTests; + sourceCoveringTests.reserve(1U << 16); // Reserve for approx. 65k source files + const AZStd::string delim = "\n"; + auto start = 0U; + auto end = sourceCoveringTestsListString.find(delim); + + while (end != AZStd::string::npos) + { + const auto line = sourceCoveringTestsListString.substr(start, end - start); + if (line.starts_with(TargetTag)) + { + // This is a test target covering the most recent source discovered + coveringTests.push_back(line.substr(1, line.length() - 1)); + } + else + { + // This is a new source file so assign the accumulated test targets to the current source file before proceeding + if (!coveringTests.empty()) + { + sourceCoveringTests.push_back(SourceCoveringTests(source, AZStd::move(coveringTests))); + coveringTests.clear(); + } + + source = line; + } + + start = end + delim.length(); + end = sourceCoveringTestsListString.find(delim, start); + } + + // Ensure we properly assign the accumulated test targets to the most recent source discovered + if (!coveringTests.empty()) + { + sourceCoveringTests.push_back(SourceCoveringTests(source, AZStd::move(coveringTests))); + coveringTests.clear(); + } + + return SourceCoveringTestsList(AZStd::move(sourceCoveringTests)); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.h new file mode 100644 index 0000000000..901e43233c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceCoveringTestsSerializer.h @@ -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 + +#include + +#include + +namespace TestImpact +{ + //! Serializes the specified source covering tests list to plain text format. + AZStd::string SerializeSourceCoveringTestsList(const SourceCoveringTestsList& sourceCoveringTestsList); + + //! Deserializes a source covering tests list from the specified source covering tests data in plain text format. + SourceCoveringTestsList DeserializeSourceCoveringTestsList(const AZStd::string& sourceCoveringTestsListString); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.cpp new file mode 100644 index 0000000000..7d4e76acd1 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.cpp @@ -0,0 +1,87 @@ +/* + * 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 + +namespace TestImpact +{ + ParentTarget::ParentTarget(const TestTarget* target) + : m_target(target) + { + } + + ParentTarget::ParentTarget(const ProductionTarget* target) + : m_target(target) + { + } + + bool ParentTarget::operator==(const ParentTarget& other) const + { + return GetBuildTarget() == other.GetBuildTarget(); + } + + const BuildTarget* ParentTarget::GetBuildTarget() const + { + const BuildTarget* buildTarget; + AZStd::visit([&buildTarget](auto&& target) + { + buildTarget = target; + + }, m_target); + + return buildTarget; + } + + const Target& ParentTarget::GetTarget() const + { + return m_target; + } + + SourceDependency::SourceDependency( + const RepoPath& path, + DependencyData&& dependencyData) + : m_path(path) + , m_dependencyData(AZStd::move(dependencyData)) + { + } + + const RepoPath& SourceDependency::GetPath() const + { + return m_path; + } + + size_t SourceDependency::GetNumParentTargets() const + { + return m_dependencyData.m_parentTargets.size(); + } + + size_t SourceDependency::GetNumCoveringTestTargets() const + { + return m_dependencyData.m_coveringTestTargets.size(); + } + + const AZStd::unordered_set& SourceDependency::GetParentTargets() const + { + return m_dependencyData.m_parentTargets; + } + + const AZStd::unordered_set& SourceDependency::GetCoveringTestTargets() const + { + return m_dependencyData.m_coveringTestTargets; + } + +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.h new file mode 100644 index 0000000000..d80c4a0540 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactSourceDependency.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. + * + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace TestImpact +{ + class ProductionTarget; + class TestTarget; + + //! Representation of a source dependency's parent target. + class ParentTarget + { + public: + //! Constructor overload for test target types. + ParentTarget(const TestTarget* target); + + //! Constructor overload for production target types. + ParentTarget(const ProductionTarget* target); + + //! Returns the base build target pointer for this parent. + const BuildTarget* GetBuildTarget() const; + + //! Returns the specialized target pointer for this parent. + const Target& GetTarget() const; + + bool operator==(const ParentTarget& other) const; + private: + Target m_target; //! The specialized target pointer for this parent. + }; +} + +namespace AZStd +{ + //! Hash function for ParentTarget types for use in maps and sets + template<> struct hash + { + size_t operator()(const TestImpact::ParentTarget& parentTarget) const noexcept + { + return reinterpret_cast(parentTarget.GetBuildTarget()); + } + }; +} + +namespace TestImpact +{ + struct DependencyData + { + AZStd::unordered_set m_parentTargets; + AZStd::unordered_set m_coveringTestTargets; + }; + + //! Test target coverage and build target dependency information for a given source file in the dynamic dependency map. + class SourceDependency + { + public: + SourceDependency( + const RepoPath& path, + DependencyData&& dependencyData); + + //! Returns the path of this source file. + const RepoPath& GetPath() const; + + //! Returns the number of parent build targets this source belongs to. + size_t GetNumParentTargets() const; + + //! Returns the number of test targets covering this source file. + size_t GetNumCoveringTestTargets() const; + + //! Returns the parent targets that this source file belongs to. + const AZStd::unordered_set& GetParentTargets() const; + + //! Returns the test targets covering this source file. + const AZStd::unordered_set& GetCoveringTestTargets() const; + private: + RepoPath m_path; //!< The path of this source file. + DependencyData m_dependencyData; //!< The dependency data for this source file. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.cpp new file mode 100644 index 0000000000..35e6679fdc --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.cpp @@ -0,0 +1,226 @@ +/* +* 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 TestImpact +{ + TestSelectorAndPrioritizer::TestSelectorAndPrioritizer( + const DynamicDependencyMap* dynamicDependencyMap, DependencyGraphDataMap&& dependencyGraphDataMap) + : m_dynamicDependencyMap(dynamicDependencyMap) + , m_dependencyGraphDataMap(AZStd::move(dependencyGraphDataMap)) + { + } + + AZStd::vector TestSelectorAndPrioritizer::SelectTestTargets( + const ChangeDependencyList& changeDependencyList, Policy::TestPrioritization testSelectionStrategy) + { + const auto selectedTestTargetAndDependerMap = SelectTestTargets(changeDependencyList); + const auto prioritizedSelectedTests = PrioritizeSelectedTestTargets(selectedTestTargetAndDependerMap, testSelectionStrategy); + return prioritizedSelectedTests; + } + + TestSelectorAndPrioritizer::SelectedTestTargetAndDependerMap TestSelectorAndPrioritizer::SelectTestTargets( + const ChangeDependencyList& changeDependencyList) + { + SelectedTestTargetAndDependerMap selectedTestTargetMap; + + // Create operations + for (const auto& sourceDependency : changeDependencyList.GetCreateSourceDependencies()) + { + for (const auto& parentTarget : sourceDependency.GetParentTargets()) + { + AZStd::visit([&selectedTestTargetMap, this](auto&& target) + { + if constexpr (IsProductionTarget) + { + // Parent Targets: Yes + // Coverage Data : No + // Source Type : Production + // + // Scenario + // 1. The file has been newly created + // 2. This file exists in one or more source to production target mapping artifacts + // 3. There exists no coverage data for this file in the source covering test list + // + // Action + // 1. Select all test targets covering the parent production targets + const auto coverage = m_dynamicDependencyMap->GetCoveringTestTargetsForProductionTarget(*target); + for (const auto* testTarget : coverage) + { + selectedTestTargetMap[testTarget].insert(target); + } + } + else + { + // Parent Targets: Yes + // Coverage Data : No + // Source Type : Test + // + // Scenario + // 1. The file has been newly created + // 2. This file exists in one or more source to test target mapping artifacts + // 3. There exists no coverage data for this file in the source covering test list + // + // Action + // 1. Select all parent test targets + selectedTestTargetMap.insert(target); + } + }, parentTarget.GetTarget()); + } + } + + // Update operations + for (const auto& sourceDependency : changeDependencyList.GetUpdateSourceDependencies()) + { + if (sourceDependency.GetNumParentTargets()) + { + if (sourceDependency.GetNumCoveringTestTargets()) + { + for (const auto& parentTarget : sourceDependency.GetParentTargets()) + { + AZStd::visit([&selectedTestTargetMap, &sourceDependency, this](auto&& target) + { + if constexpr (IsProductionTarget) + { + // Parent Targets: Yes + // Coverage Data : Yes + // Source Type : Production + // + // Scenario + // 1. The existing file has been modified + // 2. This file exists in one or more source to production target mapping artifacts + // 3. There exists coverage data for this file in the source covering test list + // + // Action + // 1. Select all test targets covering this file + for (const auto* testTarget : sourceDependency.GetCoveringTestTargets()) + { + selectedTestTargetMap[testTarget].insert(target); + } + } + else + { + // Parent Targets: Yes + // Coverage Data : Yes + // Source Type : Test + // + // Scenario + // 1. The existing file has been modified + // 2. This file exists in one or more source to test target mapping artifacts + // 3. There exists coverage data for this file in the source covering test list + // + // Action + // 1. Select the parent test targets for this file + selectedTestTargetMap.insert(target); + } + }, parentTarget.GetTarget()); + } + } + else + { + for (const auto& parentTarget : sourceDependency.GetParentTargets()) + { + AZStd::visit([&selectedTestTargetMap, &sourceDependency, this](auto&& target) + { + if constexpr (IsTestTarget) + { + // Parent Targets: Yes + // Coverage Data : No + // Source Type : Test + // + // Scenario + // 1. The existing file has been modified + // 2. This file exists in one or more source to test target mapping artifacts + // 3. There exists no coverage data for this file in the source covering test list + // + // Action + // 1. Select the parent test targets for this file + selectedTestTargetMap.insert(target); + } + }, parentTarget.GetTarget()); + } + } + } + else + { + // Parent Targets: No + // Coverage Data : Yes + // Source Type : Indeterminate + // + // Scenario + // 1. The existing file has been modified + // 2. Either: + // a) This file previously existed in one or more source to target mapping artifacts + // b) This file no longer exists in any source to target mapping artifacts + // c) The coverage data for this file was has yet to be deleted from the source covering test list + // 3. Or: + // a) The file is being used by build targets but has erroneously not been explicitly added to the build + // system (e.g. include directive pulling in a header from the repository that has not been added to + // any build targets due to an oversight) + // + // Action + // 1. Log potential orphaned source file warning + // 2. Select all test targets covering this file + // 3. Delete the existing coverage data from the source covering test list + + for (const auto* testTarget : sourceDependency.GetCoveringTestTargets()) + { + selectedTestTargetMap.insert(testTarget); + } + } + } + + // Delete operations + for (const auto& sourceDependency : changeDependencyList.GetDeleteSourceDependencies()) + { + // Parent Targets: No + // Coverage Data : Yes + // Source Type : Indeterminate + // + // Scenario + // 1. The existing file has been deleted + // 2. This file previously existed in one or more source to target mapping artifacts + // 2. This file does not exist in any source to target mapping artifacts + // 4. The coverage data for this file was has yet to be deleted from the source covering test list + // + // Action + // 1. Select all test targets covering this file + // 2. Delete the existing coverage data from the source covering test list + for (const auto* testTarget : sourceDependency.GetCoveringTestTargets()) + { + selectedTestTargetMap.insert(testTarget); + } + } + + return selectedTestTargetMap; + } + + AZStd::vector TestSelectorAndPrioritizer::PrioritizeSelectedTestTargets( + const SelectedTestTargetAndDependerMap& selectedTestTargetAndDependerMap, + [[maybe_unused]] Policy::TestPrioritization testSelectionStrategy) + { + AZStd::vector selectedTestTargets; + + // Prioritization disabled for now + // SPEC-6563 + for (const auto& [testTarget, dependerTargets] : selectedTestTargetAndDependerMap) + { + selectedTestTargets.push_back(testTarget); + } + + return selectedTestTargets; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.h new file mode 100644 index 0000000000..6ee3a616c4 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dependency/TestImpactTestSelectorAndPrioritizer.h @@ -0,0 +1,72 @@ +/* + * 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 TestImpact +{ + class DynamicDependencyMap; + class BuildTarget; + class TestTarget; + + //! Map of build targets and their dependency graph data. + //! For test targets, the dependency graph data is that of the build targets which the test target depends on. + //! For production targets, the dependency graph is that of the build targets that depend on it (dependers). + //! @note No dependency graph data is not an error, it simple means that the target cannot be prioritized. + using DependencyGraphDataMap = AZStd::unordered_map; + + //! Selects the test targets that cover a given set of changes based on the CRUD rules and optionally prioritizes the test + //! selection according to their locality of their covering production targets in the their dependency graphs. + //! @note the CRUD rules for how tests are selected can be found in the MicroRepo header file. + class TestSelectorAndPrioritizer + { + public: + //! Constructs the test selector and prioritizer for the given dynamic dependency map. + //! @param dynamicDependencyMap The dynamic dependency map representing the repository source tree. + //! @param dependencyGraphDataMap The map of build targets and their dependency graph data for use in test prioritization. + TestSelectorAndPrioritizer(const DynamicDependencyMap* dynamicDependencyMap, DependencyGraphDataMap&& dependencyGraphDataMap); + + //! Select the covering test targets for the given set of source changes and optionally prioritizes said test selection. + //! @param changeDependencyList The resolved list of source dependencies for the CRUD source changes. + //! @param testSelectionStrategy The test selection and prioritization strategy to apply to the given CRUD source changes. + AZStd::vector SelectTestTargets(const ChangeDependencyList& changeDependencyList, Policy::TestPrioritization testSelectionStrategy); + + private: + //! Map of selected test targets and the production targets they cover for the given set of source changes. + using SelectedTestTargetAndDependerMap = AZStd::unordered_map>; + + //! Selects the test targets covering the set of source changes in the change dependency list. + //! @param changeDependencyList The change dependency list containing the CRUD source changes to select tests for. + //! @returns The selected tests and their covering production targets for the given set of source changes. + SelectedTestTargetAndDependerMap SelectTestTargets(const ChangeDependencyList& changeDependencyList); + + //! Prioritizes the selected tests according to the specified test selection strategy, + //! @note If no dependency graph data exists for a given test target then that test target still be selected albeit not prioritized. + //! @param selectedTestTargetAndDependerMap The selected tests to prioritize. + //! @param testSelectionStrategy The test selection strategy to prioritize the selected tests. + //! @returns The selected tests either in either arbitrary order or in prioritized with highest priority first. + AZStd::vector PrioritizeSelectedTestTargets( + const SelectedTestTargetAndDependerMap& selectedTestTargetAndDependerMap, Policy::TestPrioritization testSelectionStrategy); + + const DynamicDependencyMap* m_dynamicDependencyMap; + DependencyGraphDataMap m_dependencyGraphDataMap; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dummy.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dummy.cpp deleted file mode 100644 index 1ac4c2487f..0000000000 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Dummy.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/Clang/testimpactframework_clang.cmake b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/Clang/testimpactframework_clang.cmake new file mode 100644 index 0000000000..af1177e359 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/Clang/testimpactframework_clang.cmake @@ -0,0 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(LY_COMPILE_OPTIONS PUBLIC -fexceptions) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/MSVC/testimpactframework_msvc.cmake b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/MSVC/testimpactframework_msvc.cmake new file mode 100644 index 0000000000..79d4b190a2 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Common/MSVC/testimpactframework_msvc.cmake @@ -0,0 +1,12 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(LY_COMPILE_OPTIONS PUBLIC /EHsc) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Dummy_Windows.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Dummy_Windows.cpp deleted file mode 100644 index 1ac4c2487f..0000000000 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Dummy_Windows.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Handle.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Handle.h new file mode 100644 index 0000000000..3930732bd0 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Handle.h @@ -0,0 +1,92 @@ +/* + * 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 TestImpact +{ + //! OS function to cleanup handle + using CleanupFunc = BOOL (*)(HANDLE); + + //! RAII wrapper around OS handles. + template + class Handle + { + public: + Handle() = default; + explicit Handle(HANDLE handle); + ~Handle(); + + operator HANDLE&(); + PHANDLE operator&(); + HANDLE& operator=(HANDLE handle); + void Close(); + + private: + HANDLE m_handle = INVALID_HANDLE_VALUE; + }; + + template + Handle::Handle(HANDLE handle) + : m_handle(handle) + { + } + + template + Handle::~Handle() + { + Close(); + } + + template + Handle::operator HANDLE&() + { + return m_handle; + } + + template + PHANDLE Handle::operator&() + { + return &m_handle; + } + + template + HANDLE& Handle::operator=(HANDLE handle) + { + // Setting the handle to INVALID_HANDLE_VALUE will close the handle + if (handle == INVALID_HANDLE_VALUE && m_handle != INVALID_HANDLE_VALUE) + { + Close(); + } + else + { + m_handle = handle; + } + + return m_handle; + } + + template + void Handle::Close() + { + if (m_handle != INVALID_HANDLE_VALUE) + { + CleanupFuncT(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } + } + + using ObjectHandle = Handle; + using WaitHandle = Handle; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.cpp new file mode 100644 index 0000000000..b872487942 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.cpp @@ -0,0 +1,67 @@ +/* + * 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 "TestImpactWin32_Pipe.h" + +#include + +#include +#include + +namespace TestImpact +{ + Pipe::Pipe(SECURITY_ATTRIBUTES& sa, HANDLE& stdChannel) + { + if (!CreatePipe(&m_parent, &m_child, &sa, 0)) + { + throw ProcessException("Couldn't create pipe"); + } + + SetHandleInformation(m_parent, HANDLE_FLAG_INHERIT, 0); + stdChannel = m_child; + } + + void Pipe::ReleaseChild() + { + m_child.Close(); + } + + void Pipe::EmptyPipe() + { + DWORD bytesAvailable = 0; + while (PeekNamedPipe(m_parent, NULL, 0, NULL, &bytesAvailable, NULL) && bytesAvailable > 0) + { + // Grow the buffer by the number of bytes available in the pipe and append the new data + DWORD bytesRead; + const size_t currentSize = m_buffer.size(); + m_buffer.resize(m_buffer.size() + bytesAvailable); + if (!ReadFile(m_parent, m_buffer.data() + currentSize, bytesAvailable, &bytesRead, NULL) || bytesRead == 0) + { + throw ProcessException("Couldn't read child output from pipe"); + } + } + } + + AZStd::string Pipe::GetContentsAndClearInternalBuffer() + { + EmptyPipe(); + AZStd::string contents; + + if (m_buffer.size() > 0) + { + contents = AZStd::string(m_buffer.begin(), m_buffer.end()); + m_buffer.clear(); + } + + return contents; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.h new file mode 100644 index 0000000000..d7b0d616f7 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Pipe.h @@ -0,0 +1,52 @@ +/* + * 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 "TestImpactWin32_Handle.h" + +#include +#include +#include + +namespace TestImpact +{ + //! RAII wrapper around OS pipes. + //! Used to connect the standard output and standard error of the child process to a sink accessible to the + //! parent process to allow the parent process to read the output(s) of the child process. + class Pipe + { + public: + Pipe(SECURITY_ATTRIBUTES& sa, HANDLE& stdChannel); + Pipe(Pipe&& other) = delete; + Pipe(Pipe& other) = delete; + Pipe& operator=(Pipe& other) = delete; + Pipe& operator=(Pipe&& other) = delete; + + //! Releases the child end of the pipe (not needed once parent has their end). + void ReleaseChild(); + + //! Empties the contents of the pipe into the internal buffer. + void EmptyPipe(); + + //! Empties the contents of the pipe into a string, clearing the internal buffer. + AZStd::string GetContentsAndClearInternalBuffer(); + + private: + // Parent and child process ends of pipe + ObjectHandle m_parent; + ObjectHandle m_child; + + // Buffer for emptying pipe upon child processes exit + AZStd::vector m_buffer; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.cpp new file mode 100644 index 0000000000..06dd4e846f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.cpp @@ -0,0 +1,259 @@ +/* + * 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 "TestImpactWin32_Process.h" + +#include + +#include + +namespace TestImpact +{ + // Note: this is called from an OS thread + VOID ProcessWin32::ProcessExitCallback(PVOID processPtr, [[maybe_unused]] BOOLEAN EventSignalled) + { + // Lock the process destructor from being entered from the client thread + AZStd::lock_guard lifeLock(m_lifeCycleMutex); + + ProcessId id = reinterpret_cast(processPtr); + auto process = m_masterProcessList[id]; + + // Check that the process hasn't already been destructed from the client thread + if (process && process->m_isRunning) + { + // Lock state access and/or mutation from the client thread + AZStd::lock_guard stateLock(process->m_stateMutex); + process->RetrieveOSReturnCodeAndCleanUpProcess(); + } + } + + ProcessWin32::ProcessWin32(const ProcessInfo& processInfo) + : Process(processInfo) + { + AZStd::string args(m_processInfo.GetProcessPath().String()); + + if (m_processInfo.GetStartupArgs().length()) + { + args = AZStd::string::format("%s %s", args.c_str(), m_processInfo.GetStartupArgs().c_str()); + } + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = IsPiping(); + + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + CreatePipes(sa, si); + + if (!CreateProcess( + NULL, + &args[0], + NULL, + NULL, + IsPiping(), + CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW, + NULL, NULL, + &si, &pi)) + { + throw ProcessException(AZStd::string::format("Couldn't create process with args: %s", args.c_str())); + } + + ReleaseChildPipes(); + + m_process = pi.hProcess; + m_thread = pi.hThread; + m_isRunning = true; + + { + // Lock reading of the master process list from the OS thread + AZStd::lock_guard lock(m_lifeCycleMutex); + + // Register this process with a unique id in the master process list + m_uniqueId = m_uniqueIdCounter++; + m_masterProcessList[m_uniqueId] = this; + } + + // Register the process exit signal callback + if (!RegisterWaitForSingleObject( + &m_waitCallback, + pi.hProcess, + ProcessExitCallback, + reinterpret_cast(m_uniqueId), + INFINITE, + WT_EXECUTEONLYONCE)) + { + throw ProcessException("Couldn't register wait object for process exit event"); + } + } + + bool ProcessWin32::IsPiping() const + { + return m_processInfo.ParentHasStdOutput() || m_processInfo.ParentHasStdError(); + } + + void ProcessWin32::CreatePipes(SECURITY_ATTRIBUTES& sa, STARTUPINFO& si) + { + if (IsPiping()) + { + si.dwFlags = STARTF_USESTDHANDLES; + + if (m_processInfo.ParentHasStdOutput()) + { + m_stdOutPipe.emplace(sa, si.hStdOutput); + } + + if (m_processInfo.ParentHasStdError()) + { + m_stdErrPipe.emplace(sa, si.hStdError); + } + } + } + + void ProcessWin32::ReleaseChildPipes() + { + if (m_stdOutPipe) + { + m_stdOutPipe->ReleaseChild(); + } + + if (m_stdErrPipe) + { + m_stdErrPipe->ReleaseChild(); + } + } + + void ProcessWin32::EmptyPipes() + { + if (m_stdOutPipe) + { + m_stdOutPipe->EmptyPipe(); + } + + if (m_stdErrPipe) + { + m_stdErrPipe->EmptyPipe(); + } + } + + AZStd::optional ProcessWin32::ConsumeStdOut() + { + if (m_stdOutPipe) + { + AZStd::string contents = m_stdOutPipe->GetContentsAndClearInternalBuffer(); + if (!contents.empty()) + { + return contents; + } + } + + return AZStd::nullopt; + } + + AZStd::optional ProcessWin32::ConsumeStdErr() + { + if (m_stdErrPipe) + { + AZStd::string contents = m_stdErrPipe->GetContentsAndClearInternalBuffer(); + if (!contents.empty()) + { + return contents; + } + } + + return AZStd::nullopt; + } + + void ProcessWin32::Terminate(ReturnCode returnCode) + { + // Lock process cleanup from the OS thread + AZStd::lock_guard lock(m_stateMutex); + + if (m_isRunning) + { + // Cancel the callback so we can wait for the signal ourselves + // Note: we keep the state mutex locked as closing the callback is not guaranteed to be instantaneous + m_waitCallback.Close(); + + // Terminate the process and set the error code to the terminate code + TerminateProcess(m_process, returnCode); + SetReturnCodeAndCleanUpProcesses(returnCode); + } + } + + bool ProcessWin32::IsRunning() const + { + return m_isRunning; + } + + void ProcessWin32::BlockUntilExit() + { + // Lock process cleanup from the OS thread + AZStd::lock_guard lock(m_stateMutex); + + if (m_isRunning) + { + // Cancel the callback so we can wait for the signal ourselves + // Note: we keep the state mutex locked as closing the callback is not guaranteed to be instantaneous + m_waitCallback.Close(); + + if (IsPiping()) + { + // This process will be blocked from exiting if pipe not emptied so will deadlock if we wait + // indefintely whilst there is still output in the pipes so instead keep waiting and checking + // if the pipes need emptying until the process exits + while (WAIT_OBJECT_0 != WaitForSingleObject(m_process, 1)) + { + EmptyPipes(); + } + } + else + { + // No possibility of pipe deadlocking, safe to wait indefinitely for process exit + WaitForSingleObject(m_process, INFINITE); + } + + // Now that the this process has definately exited we are safe to clean up + RetrieveOSReturnCodeAndCleanUpProcess(); + } + } + + void ProcessWin32::RetrieveOSReturnCodeAndCleanUpProcess() + { + DWORD returnCode; + GetExitCodeProcess(m_process, &returnCode); + SetReturnCodeAndCleanUpProcesses(returnCode); + } + + void ProcessWin32::SetReturnCodeAndCleanUpProcesses(ReturnCode returnCode) + { + m_returnCode = returnCode; + m_process.Close(); + m_thread.Close(); + m_waitCallback.Close(); + m_isRunning = false; + } + + ProcessWin32::~ProcessWin32() + { + // Lock the process exit signal callback from being entered OS thread + AZStd::lock_guard lock(m_lifeCycleMutex); + + // Remove this process from the master list so the process exit signal doesn't attempt to cleanup + // this process if it is deleted client side + m_masterProcessList[m_uniqueId] = nullptr; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.h new file mode 100644 index 0000000000..a29fecada1 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_Process.h @@ -0,0 +1,101 @@ +/* + * 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 "TestImpactWin32_Handle.h" +#include "TestImpactWin32_Pipe.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace TestImpact +{ + //! Platform-specific implementation of Process. + class ProcessWin32 + : public Process + { + public: + explicit ProcessWin32(const ProcessInfo& processInfo); + ProcessWin32(ProcessWin32&& other) = delete; + ProcessWin32(ProcessWin32& other) = delete; + ProcessWin32& operator=(ProcessWin32& other) = delete; + ProcessWin32& operator=(ProcessWin32&& other) = delete; + ~ProcessWin32(); + + // Process overrides... + void Terminate(ReturnCode returnCode) override; + void BlockUntilExit() override; + bool IsRunning() const override; + AZStd::optional ConsumeStdOut() override; + AZStd::optional ConsumeStdErr() override; + + private: + //! Callback for process exit signal. + static VOID ProcessExitCallback(PVOID processPtr, BOOLEAN EventSignalled); + + //! Retrieves the return code and cleans up the OS handles. + void RetrieveOSReturnCodeAndCleanUpProcess(); + + //! Sets the return code and cleans up the OS handles + void SetReturnCodeAndCleanUpProcesses(ReturnCode returnCode); + + //! Returns true if either stdout or stderr is beign redirected. + bool IsPiping() const; + + //! Creates the parent and child pipes for stdout and/or stderr. + void CreatePipes(SECURITY_ATTRIBUTES& sa, STARTUPINFO& si); + + //! Empties all pipes so the process can exit without deadlocking. + void EmptyPipes(); + + //! Releases the child end of the stdout and/or stderr pipes/ + void ReleaseChildPipes(); + + // Flag to determine whether or not the process is in flight + AZStd::atomic_bool m_isRunning = false; + + // Unique id assigned to this process (not the same as the id assigned by the client in the ProcessInfo class) + // as used in the master process list + size_t m_uniqueId = 0; + + // Handles to OS process + ObjectHandle m_process; + ObjectHandle m_thread; + + // Handle to process exit signal callback + WaitHandle m_waitCallback; + + // Process to parent standard output piping + AZStd::optional m_stdOutPipe; + AZStd::optional m_stdErrPipe; + + // Mutex protecting process state access/mutation from the OS thread and client thread + mutable AZStd::mutex m_stateMutex; + + // Mutex keeping the process life cycles in sync between the OS thread and client thread + inline static AZStd::mutex m_lifeCycleMutex; + + // Unique counter to give each launched process a unique id + inline static size_t m_uniqueIdCounter = 1; + + // Master process list used to ensure consistency of process lifecycles between OS thread and client thread + inline static std::unordered_map m_masterProcessList; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_ProcessLauncher.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_ProcessLauncher.cpp new file mode 100644 index 0000000000..d798e4ff7b --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/Process/TestImpactWin32_ProcessLauncher.cpp @@ -0,0 +1,24 @@ +/* + * 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 "TestImpactWin32_Process.h" + +#include +#include + +namespace TestImpact +{ + AZStd::unique_ptr LaunchProcess(const ProcessInfo& processInfo) + { + return AZStd::make_unique(processInfo); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/JobRunner/TestImpactWin32_TestTargetExtension.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/JobRunner/TestImpactWin32_TestTargetExtension.cpp new file mode 100644 index 0000000000..14a1a28ba7 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/JobRunner/TestImpactWin32_TestTargetExtension.cpp @@ -0,0 +1,44 @@ +/* + * 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 TestImpact +{ + AZStd::string GetTestTargetExtension(const TestTarget* testTarget) + { + static constexpr char* const standAloneExtension = ".exe"; + static constexpr char* const testRunnerExtension = ".dll"; + + switch (const auto launchMethod = testTarget->GetLaunchMethod(); launchMethod) + { + case LaunchMethod::StandAlone: + { + return standAloneExtension; + } + case LaunchMethod::TestRunner: + { + return testRunnerExtension; + } + default: + { + throw TestEngineException( + AZStd::string::format( + "Unexpected launch method for target %s: %u", + testTarget->GetName().c_str(), + aznumeric_cast(launchMethod))); + } + } + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/TestImpactWin32_TestEngineJobFailure.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/TestImpactWin32_TestEngineJobFailure.cpp new file mode 100644 index 0000000000..6561293d5a --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/TestEngine/TestImpactWin32_TestEngineJobFailure.cpp @@ -0,0 +1,35 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + // Known error codes for test instrumentation + namespace ErrorCodes + { + namespace OpenCppCoverage + { + static constexpr ReturnCode InvalidArgs = 0x9F8C8E5C; + } + } + + AZStd::optional CheckForKnownTestInstrumentErrorCode(ReturnCode returnCode) + { + if (returnCode == ErrorCodes::OpenCppCoverage::InvalidArgs) + { + return Client::TestRunResult::FailedToExecute; + } + + return AZStd::nullopt; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/platform_windows_files.cmake b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/platform_windows_files.cmake index 9b83a02475..e994bcdbbd 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/platform_windows_files.cmake +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Platform/Windows/platform_windows_files.cmake @@ -10,5 +10,12 @@ # set(FILES - Dummy_Windows.cpp + Process/TestImpactWin32_ProcessLauncher.cpp + Process/TestImpactWin32_Process.cpp + Process/TestImpactWin32_Process.h + Process/TestImpactWin32_Handle.h + Process/TestImpactWin32_Pipe.cpp + Process/TestImpactWin32_Pipe.h + TestEngine/JobRunner/TestImpactWin32_TestTargetExtension.cpp + TestEngine/TestImpactWin32_TestEngineJobFailure.cpp ) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJob.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJob.h new file mode 100644 index 0000000000..47afe1ca3e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJob.h @@ -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. + * + */ + +#pragma once + +#include +#include + +namespace TestImpact +{ + //! Representation of a unit of work to be performed by a process. + //! @tparam JobInfoT The JobInfo structure containing the information required to run this job. + //! @tparam JobPayloadT The resulting output of the processed artifact produced by this job. + template + class Job + : public JobMetaWrapper + { + public: + using Info = JobInfoT; + using Payload = JobPayloadT; + + //! Constructor with r-values for the specific use case of the job runner. + Job(const Info& jobInfo, JobMeta&& jobMeta, AZStd::optional&& payload); + + //! Returns the job info associated with this job. + const Info& GetJobInfo() const; + + //! Returns the payload produced by this job. + const AZStd::optional& GetPayload() const; + + //! Facilitates the client consuming the payload. + //! @note It is valid for a job life cycle to continue after having released its payload. + AZStd::optional ReleasePayload(); + + private: + Info m_jobInfo; + AZStd::optional m_payload; + }; + + template + Job::Job(const Info& jobInfo, JobMeta&& jobMeta, AZStd::optional&& payload) + : JobMetaWrapper(AZStd::move(jobMeta)) + , m_jobInfo(jobInfo) + , m_payload(AZStd::move(payload)) + { + } + + template + const JobInfoT& Job::GetJobInfo() const + { + return m_jobInfo; + } + + template + const AZStd::optional& Job::GetPayload() const + { + return m_payload; + } + + template + AZStd::optional Job::ReleasePayload() + { + return AZStd::exchange(m_payload, AZStd::nullopt); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobInfo.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobInfo.h new file mode 100644 index 0000000000..fbdad82c12 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobInfo.h @@ -0,0 +1,81 @@ +/* + * 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 TestImpact +{ + //! Per-job information to configure and run jobs and process the resulting artifacts. + //! @tparam AdditionalInfo Additional information to be provided to each job to be consumed by client. + template + class JobInfo + : public AdditionalInfo + { + public: + using IdType = size_t; + using CommandType = AZStd::string; + + //! Client-provided identifier to distinguish between different jobs. + //! @note Ids of different job types are not interchangeable. + struct Id + { + IdType m_value; + }; + + //! Command used my ProcessScheduler to execute this job. + //! @note Commands of different job types are not interchangeable. + struct Command + { + CommandType m_args; + }; + + //! Constructs the job information with any additional information required by the job. + //! @param jobId The client-provided unique identifier for the job. + //! @param command The command used to launch the process running the job. + //! @param additionalInfo The arguments to be provided to the additional information data structure. + template + JobInfo(Id jobId, const Command& command, AdditionalInfoArgs&&... additionalInfo); + + //! Returns the id of this job. + Id GetId() const; + + //! Returns the command arguments used to execute this job. + const Command& GetCommand() const; + + private: + Id m_id; + Command m_command; + }; + + template + template + JobInfo::JobInfo(Id jobId, const Command& command, AdditionalInfoArgs&&... additionalInfo) + : AdditionalInfo{std::forward(additionalInfo)...} + , m_id(jobId) + , m_command(command) + { + } + + template + typename JobInfo::Id JobInfo::GetId() const + { + return m_id; + } + + template + const typename JobInfo::Command& JobInfo::GetCommand() const + { + return m_command; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.cpp new file mode 100644 index 0000000000..022115c042 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.cpp @@ -0,0 +1,61 @@ +/* + * 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 TestImpact +{ + JobMetaWrapper::JobMetaWrapper(const JobMeta& jobMeta) + : m_meta(jobMeta) + { + } + + JobMetaWrapper::JobMetaWrapper(JobMeta&& jobMeta) + : m_meta(AZStd::move(jobMeta)) + { + } + + JobResult JobMetaWrapper::GetJobResult() const + { + return m_meta.m_result; + } + + AZStd::optional JobMetaWrapper::GetReturnCode() const + { + return m_meta.m_returnCode; + } + + AZStd::chrono::high_resolution_clock::time_point JobMetaWrapper::GetStartTime() const + { + return m_meta.m_startTime.value_or(AZStd::chrono::high_resolution_clock::time_point()); + } + + AZStd::chrono::high_resolution_clock::time_point JobMetaWrapper::GetEndTime() const + { + if (m_meta.m_startTime.has_value() && m_meta.m_duration.has_value()) + { + return m_meta.m_startTime.value() + m_meta.m_duration.value(); + } + else + { + return AZStd::chrono::high_resolution_clock::time_point(); + } + } + + AZStd::chrono::milliseconds JobMetaWrapper::GetDuration() const + { + return m_meta.m_duration.value_or(AZStd::chrono::milliseconds{ 0 }); + } + +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.h new file mode 100644 index 0000000000..c8f459ba6f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobMeta.h @@ -0,0 +1,68 @@ +/* + * 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 TestImpact +{ + //! Result of a job that was run. + enum class JobResult + { + NotExecuted, //!< The job was not executed (e.g. the job runner terminated before the job could be executed). + FailedToExecute, //!< The job failed to execute (e.g. due to the arguments used to execute the job being invalid). + Timeout, //!< The job was terminated by the job runner (e.g. job timeout exceeded while job was in-flight). + Terminated, //!< The job was terminated by the job runner (e.g. global timeout exceeded while job was in-flight). + ExecutedWithFailure, //!< The job was executed but exited in an erroneous state (the underlying process returned non-zero). + ExecutedWithSuccess //!< The job was executed and exited in a successful state (the underlying processes returned zero). + }; + + //! The meta-data for a given job. + struct JobMeta + { + JobResult m_result = JobResult::NotExecuted; + AZStd::optional m_startTime; //!< The time, relative to the job runner start, that this job started. + AZStd::optional m_duration; //!< The duration that this job took to complete. + AZStd::optional m_returnCode; //!< The return code of the underlying processes of this job. + }; + + //! Wrapper for job meta structure to inheritance/aggregation without being coupled to the JobInfo or Job classes. + class JobMetaWrapper + { + public: + JobMetaWrapper(const JobMeta& jobMeta); + JobMetaWrapper(JobMeta&& jobMeta); + + //! Returns the result of this job. + JobResult GetJobResult() const; + + //! Returns the start time, relative to the job runner start, that this job started. + AZStd::chrono::high_resolution_clock::time_point GetStartTime() const; + + //! Returns the end time, relative to the job runner start, that this job ended. + AZStd::chrono::high_resolution_clock::time_point GetEndTime() const; + + //! Returns the duration that this job took to complete. + AZStd::chrono::milliseconds GetDuration() const; + + //! Returns the return code of the underlying processes of this job. + AZStd::optional GetReturnCode() const; + + private: + JobMeta m_meta; + }; +} // namespace TestImpact + diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobRunner.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobRunner.h new file mode 100644 index 0000000000..7d726cecc1 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/JobRunner/TestImpactProcessJobRunner.h @@ -0,0 +1,183 @@ +/* + * 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 TestImpact +{ + //! Callback for job completion/failure. + //! @param jobInfo The job information associated with this job. + //! @param meta The meta-data about the job run. + //! @param std The standard output and standard error of the process running the job. + template + using JobCallback = AZStd::function; + + //! The payloads produced by the job-specific payload producer in the form of a map associating each job id with the job's payload. + template + using PayloadMap = AZStd::unordered_map>; + + //! The map used by the client to associate the job information and meta-data with the job ids. + template + using JobDataMap = AZStd::unordered_map>; + + //! The callback for producing the payloads for the jobs after all jobs have finished executing. + //! @param jobInfos The information for each job run. + //! @param jobDataMap The job data (in the form of job info and meta-data) for each job run. + template + using PayloadMapProducer = AZStd::function(const JobDataMap& jobDataMap)>; + + //! Generic job runner that launches a process for each job, records metrics about each job run and hands the payload artifacts + //! produced by each job to the client before compositing the metrics and payload artifacts for each job into a single interface + //! to be consumed by the client. + template + class JobRunner + { + public: + //! Constructs the job runner with the specified parameters to constrain job runs. + //! @param maxConcurrentProcesses he maximum number of concurrent jobs in-flight. + explicit JobRunner(size_t maxConcurrentProcesses); + + //! Executes the specified jobs and returns the products of their labor. + //! @param jobs The arguments (and other pertinent information) required for each job to be run. + //! @param stdOutRouting The standard output routing to be specified for all jobs. + //! @param stdErrRouting The standard error routing to be specified for all jobs. + //! @param jobTimeout The maximum duration a job may be in-flight before being forcefully terminated (nullopt if no timeout). + //! @param runnerTimeout The maximum duration the scheduler may run before forcefully terminating all in-flight jobs (nullopt if no timeout). + //! @param payloadMapProducer The client callback to be called when all jobs have finished to transform the work produced by each job into the desired output. + //! @param jobCallback The client callback to be called when each job changes state. + //! @return The result of the run sequence and the jobs with their associated payloads. + AZStd::pair> Execute( + const AZStd::vector& jobs, + PayloadMapProducer payloadMapProducer, + StdOutputRouting stdOutRouting, + StdErrorRouting stdErrRouting, + AZStd::optional jobTimeout, + AZStd::optional runnerTimeout, + JobCallback jobCallback); + + private: + ProcessScheduler m_processScheduler; + StdOutputRouting m_stdOutRouting; //!< Standard output routing from each job process to job runner. + StdErrorRouting m_stdErrRouting; //!< Standard error routing from each job process to job runner + AZStd::optional m_jobTimeout; //!< Maximum time a job can run for before being forcefully terminated. + AZStd::optional m_runnerTimeout; //!< Maximum time the job runner can run before forcefully terminating all in-flight jobs and shutting down. + }; + + template + JobRunner::JobRunner(size_t maxConcurrentProcesses) + : m_processScheduler(maxConcurrentProcesses) + { + } + + template + AZStd::pair> JobRunner::Execute( + const AZStd::vector& jobInfos, + PayloadMapProducer payloadMapProducer, + StdOutputRouting stdOutRouting, + StdErrorRouting stdErrRouting, + AZStd::optional jobTimeout, + AZStd::optional runnerTimeout, + JobCallback jobCallback) + { + AZStd::vector processes; + AZStd::unordered_map> metas; + AZStd::vector jobs; + jobs.reserve(jobInfos.size()); + processes.reserve(jobInfos.size()); + + // Transform the job infos into the underlying process infos required for each job + for (size_t jobIndex = 0; jobIndex < jobInfos.size(); jobIndex++) + { + const auto* jobInfo = &jobInfos[jobIndex]; + const auto jobId = jobInfo->GetId().m_value; + metas.emplace(jobId, AZStd::pair{JobMeta{}, jobInfo}); + processes.emplace_back(jobId, stdOutRouting, stdErrRouting, jobInfo->GetCommand().m_args); + } + + // Wrapper around low-level process launch callback to gather job meta-data and present a simplified callback interface to the client + const ProcessLaunchCallback processLaunchCallback = [&jobCallback, &jobInfos, &metas]( + TestImpact::ProcessId pid, + TestImpact::LaunchResult launchResult, + AZStd::chrono::high_resolution_clock::time_point createTime) + { + auto& [meta, jobInfo] = metas.at(pid); + if (launchResult == LaunchResult::Failure) + { + meta.m_result = JobResult::FailedToExecute; + return jobCallback(*jobInfo, meta, {}); + } + else + { + meta.m_startTime = createTime; + return ProcessCallbackResult::Continue; + } + }; + + // Wrapper around low-level process exit callback to gather job meta-data and present a simplified callback interface to the client + const ProcessExitCallback processExitCallback = [&jobCallback, &jobInfos, &metas]( + TestImpact::ProcessId pid, + TestImpact::ExitCondition exitCondition, + TestImpact::ReturnCode returnCode, + TestImpact::StdContent&& std, + AZStd::chrono::high_resolution_clock::time_point exitTime) + { + auto& [meta, jobInfo] = metas.at(pid); + meta.m_returnCode = returnCode; + meta.m_duration = AZStd::chrono::duration_cast(exitTime - *meta.m_startTime); + if (exitCondition == ExitCondition::Gracefull && returnCode == 0) + { + meta.m_result = JobResult::ExecutedWithSuccess; + } + else if (exitCondition == ExitCondition::Terminated) + { + meta.m_result = JobResult::Terminated; + } + else if (exitCondition == ExitCondition::Timeout) + { + meta.m_result = JobResult::Timeout; + } + else + { + meta.m_result = JobResult::ExecutedWithFailure; + } + + return jobCallback(*jobInfo, meta, AZStd::move(std)); + }; + + // Schedule all jobs for execution + const auto result = m_processScheduler.Execute( + processes, + jobTimeout, + runnerTimeout, + processLaunchCallback, + processExitCallback); + + // Hand off the jobs to the client for payload generation + auto payloadMap = payloadMapProducer(metas); + + // Unpack the payload map produced by the client into a vector of jobs containing the job data and payload for each job + for (const auto& jobInfo : jobInfos) + { + const auto jobId = jobInfo.GetId().m_value; + jobs.emplace_back(JobT(jobInfo, AZStd::move(metas.at(jobId).first), AZStd::move(payloadMap[jobId]))); + } + + return { result, jobs }; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.cpp new file mode 100644 index 0000000000..065caf54bd --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.cpp @@ -0,0 +1,321 @@ +/* + * 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 + +namespace TestImpact +{ + struct ProcessInFlight + { + AZStd::unique_ptr m_process; + AZStd::optional m_startTime; + AZStd::string m_stdOutput; + AZStd::string m_stdError; + }; + + class ProcessScheduler::ExecutionState + { + public: + ExecutionState( + size_t maxConcurrentProcesses, + AZStd::optional processTimeout, + AZStd::optional scheduleTimeout, + ProcessLaunchCallback& processLaunchCallback, + ProcessExitCallback& processExitCallback); + ~ExecutionState(); + + ProcessSchedulerResult MonitorProcesses(const AZStd::vector& processes); + void TerminateAllProcesses(ExitCondition exitStatus); + private: + ProcessCallbackResult PopAndLaunch(ProcessInFlight& processInFlight); + StdContent ConsumeProcessStdContent(ProcessInFlight& processInFlight); + void AccumulateProcessStdContent(ProcessInFlight& processInFlight); + + size_t m_maxConcurrentProcesses = 0; + ProcessLaunchCallback m_processLaunchCallback; + ProcessExitCallback m_processExitCallback; + AZStd::optional m_processTimeout; + AZStd::optional m_scheduleTimeout; + AZStd::chrono::high_resolution_clock::time_point m_startTime; + AZStd::vector m_processPool; + AZStd::queue m_processQueue; + }; + + ProcessScheduler::ExecutionState::ExecutionState( + size_t maxConcurrentProcesses, + AZStd::optional processTimeout, + AZStd::optional scheduleTimeout, + ProcessLaunchCallback& processLaunchCallback, + ProcessExitCallback& processExitCallback) + : m_maxConcurrentProcesses(maxConcurrentProcesses) + , m_processLaunchCallback(processLaunchCallback) + , m_processExitCallback(processExitCallback) + , m_processTimeout(processTimeout) + , m_scheduleTimeout(scheduleTimeout) + { + AZ_TestImpact_Eval( + !m_processTimeout.has_value() || m_processTimeout->count() > 0, ProcessException, + "Process timeout must be empty or non-zero value"); + AZ_TestImpact_Eval( + !m_scheduleTimeout.has_value() || m_scheduleTimeout->count() > 0, ProcessException, + "Scheduler timeout must be empty or non-zero value"); + } + + ProcessScheduler::ExecutionState::~ExecutionState() + { + TerminateAllProcesses(ExitCondition::Terminated); + } + + ProcessSchedulerResult ProcessScheduler::ExecutionState::MonitorProcesses(const AZStd::vector& processes) + { + AZ_TestImpact_Eval(!processes.empty(), ProcessException, "Number of processes to launch cannot be 0"); + m_startTime = AZStd::chrono::high_resolution_clock::now(); + const size_t numConcurrentProcesses = AZStd::min(processes.size(), m_maxConcurrentProcesses); + m_processPool.resize(numConcurrentProcesses); + + for (const auto& process : processes) + { + m_processQueue.emplace(process); + } + + for (auto& process : m_processPool) + { + if (PopAndLaunch(process) == ProcessCallbackResult::Abort) + { + // Client chose to abort the scheduler + TerminateAllProcesses(ExitCondition::Terminated); + return ProcessSchedulerResult::UserAborted; + } + } + + while (true) + { + // Check to see whether or not the scheduling has exceeded its specified runtime + if (m_scheduleTimeout.has_value()) + { + const auto shedulerRunTime = AZStd::chrono::milliseconds(AZStd::chrono::high_resolution_clock::now() - m_startTime); + + if (shedulerRunTime > m_scheduleTimeout) + { + // Runtime exceeded, terminate all proccesses and schedule no further + TerminateAllProcesses(ExitCondition::Timeout); + return ProcessSchedulerResult::Timeout; + } + } + + // Flag to determine whether or not there are currently any processes in-flight + bool processesInFlight = false; + + // Loop round the process pool and visit round robin queued up processes for launch + for (auto& processInFlight : m_processPool) + { + if (processInFlight.m_process) + { + // Process is alive (note: not necessarily currently running) + AccumulateProcessStdContent(processInFlight); + const ProcessId processId = processInFlight.m_process->GetProcessInfo().GetId(); + + if (!processInFlight.m_process->IsRunning()) + { + // Process has exited of its own accord + const ReturnCode returnCode = processInFlight.m_process->GetReturnCode().value(); + processInFlight.m_process.reset(); + const auto exitTime = AZStd::chrono::high_resolution_clock::now(); + + // Inform the client that the processes has exited + if (ProcessCallbackResult::Abort == m_processExitCallback( + processId, + ExitCondition::Gracefull, + returnCode, + ConsumeProcessStdContent(processInFlight), + exitTime)) + { + // Client chose to abort the scheduler + TerminateAllProcesses(ExitCondition::Terminated); + return ProcessSchedulerResult::UserAborted; + } + else if (!m_processQueue.empty()) + { + // This slot in the pool is free so launch one of the processes waiting in the queue + if (PopAndLaunch(processInFlight) == ProcessCallbackResult::Abort) + { + // Client chose to abort the scheduler + TerminateAllProcesses(ExitCondition::Terminated); + return ProcessSchedulerResult::UserAborted; + } + else + { + // We know from the above PopAndLaunch there is at least one process in-flight this iteration + processesInFlight = true; + } + } + } + else + { + // Process is still in-flight + const auto exitTime = AZStd::chrono::high_resolution_clock::now(); + const auto runTime = AZStd::chrono::milliseconds(exitTime - processInFlight.m_startTime.value()); + + // Check to see whether or not the processes has exceeded its specified flight time + if (m_processTimeout.has_value() && runTime > m_processTimeout) + { + processInFlight.m_process->Terminate(ProcessTimeoutErrorCode); + const ReturnCode returnCode = processInFlight.m_process->GetReturnCode().value(); + processInFlight.m_process.reset(); + + if (ProcessCallbackResult::Abort == m_processExitCallback( + processId, + ExitCondition::Timeout, + returnCode, + ConsumeProcessStdContent(processInFlight), + exitTime)) + { + // Client chose to abort the scheduler + TerminateAllProcesses(ExitCondition::Terminated); + return ProcessSchedulerResult::UserAborted; + } + } + + // We know that at least this process is in-flight this iteration + processesInFlight = true; + } + } + else + { + // Queue is empty, no more processes to launch + if (!m_processQueue.empty()) + { + if (PopAndLaunch(processInFlight) == ProcessCallbackResult::Abort) + { + // Client chose to abort the scheduler + TerminateAllProcesses(ExitCondition::Terminated); + return ProcessSchedulerResult::UserAborted; + } + else + { + // We know from the above PopAndLaunch there is at least one process in-flight this iteration + processesInFlight = true; + } + } + } + } + + if (!processesInFlight) + { + break; + } + } + + return ProcessSchedulerResult::Graceful; + } + + ProcessCallbackResult ProcessScheduler::ExecutionState::PopAndLaunch(ProcessInFlight& processInFlight) + { + auto processInfo = m_processQueue.front(); + m_processQueue.pop(); + const auto createTime = AZStd::chrono::high_resolution_clock::now(); + LaunchResult createResult = LaunchResult::Success; + + try + { + processInFlight.m_process = LaunchProcess(AZStd::move(processInfo)); + processInFlight.m_startTime = createTime; + } + catch (ProcessException& e) + { + AZ_Warning("ProcessScheduler", false, e.what()); + createResult = LaunchResult::Failure; + } + + return m_processLaunchCallback(processInfo.GetId(), createResult, createTime); + } + + void ProcessScheduler::ExecutionState::AccumulateProcessStdContent(ProcessInFlight& processInFlight) + { + // Accumulate the stdout/stderr so we don't deadlock with the process waiting for the pipe to empty before finishing + processInFlight.m_stdOutput += processInFlight.m_process->ConsumeStdOut().value_or(""); + processInFlight.m_stdError += processInFlight.m_process->ConsumeStdErr().value_or(""); + } + + StdContent ProcessScheduler::ExecutionState::ConsumeProcessStdContent(ProcessInFlight& processInFlight) + { + return + { + !processInFlight.m_stdOutput.empty() + ? AZStd::optional{AZStd::move(processInFlight.m_stdOutput)} + : AZStd::nullopt, + !processInFlight.m_stdError.empty() + ? AZStd::optional{AZStd::move(processInFlight.m_stdError)} + : AZStd::nullopt + }; + } + + void ProcessScheduler::ExecutionState::TerminateAllProcesses(ExitCondition exitStatus) + { + bool isCallingBackToClient = true; + const ReturnCode returnCode = static_cast(exitStatus); + + for (auto& processInFlight : m_processPool) + { + if (processInFlight.m_process) + { + processInFlight.m_process->Terminate(ProcessTerminateErrorCode); + AccumulateProcessStdContent(processInFlight); + const ProcessId processId = processInFlight.m_process->GetProcessInfo().GetId(); + + if (isCallingBackToClient) + { + const auto exitTime = AZStd::chrono::high_resolution_clock::now(); + if (ProcessCallbackResult::Abort == m_processExitCallback( + processInFlight.m_process->GetProcessInfo().GetId(), + exitStatus, + returnCode, + ConsumeProcessStdContent(processInFlight), + exitTime)) + { + // Client chose to abort the scheduler, do not make any further callbacks + isCallingBackToClient = false; + } + } + + processInFlight.m_process.reset(); + } + } + } + + ProcessScheduler::ProcessScheduler(size_t maxConcurrentProcesses) + : m_maxConcurrentProcesses(maxConcurrentProcesses) + { + AZ_TestImpact_Eval(maxConcurrentProcesses != 0, ProcessException, "Max Number of concurrent processes in flight cannot be 0"); + } + + ProcessScheduler::~ProcessScheduler() = default; + + ProcessSchedulerResult ProcessScheduler::Execute( + const AZStd::vector& processes, + AZStd::optional processTimeout, + AZStd::optional scheduleTimeout, + ProcessLaunchCallback processLaunchCallback, + ProcessExitCallback processExitCallback) + { + AZ_TestImpact_Eval(!m_executionState, ProcessException, "Couldn't execute schedule, schedule already in progress"); + m_executionState = AZStd::make_unique( + m_maxConcurrentProcesses, processTimeout, scheduleTimeout, processLaunchCallback, processExitCallback); + const auto result = m_executionState->MonitorProcesses(processes); + m_executionState.reset(); + return result; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.h new file mode 100644 index 0000000000..769bf59ff8 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/Scheduler/TestImpactProcessScheduler.h @@ -0,0 +1,115 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace TestImpact +{ + //! Result of the attempt to launch a process. + enum class LaunchResult : bool + { + Failure, + Success + }; + + //! The condition under which the processes exited. + //! @note For convinience, the terminate and timeout condition values are set to the corresponding return value sent to the + //! process. + enum class ExitCondition : ReturnCode + { + Gracefull, //!< Process has exited of its own accord. + Terminated = ProcessTerminateErrorCode, //!< The process was terminated by the client/scheduler. + Timeout = ProcessTimeoutErrorCode //!< The process was terminated by the scheduler due to exceeding runtime limit. + }; + + //! Client result for process scheduler callbacks. + enum class ProcessCallbackResult : bool + { + Continue, //!< Continune scheduling. + Abort //!< Abort scheduling immediately. + }; + + //! Result of the process scheduling sequence. + enum class ProcessSchedulerResult : AZ::u8 + { + Graceful, //!< The scheduler completed its run without incident or was terminated gracefully in response to a client callback result. + UserAborted, //!< The scheduler aborted prematurely due to the user returning an abort value from thier callback handler. + Timeout //!< The scheduler aborted its run prematurely due to its runtime exceeding the scheduler timeout value. + }; + + //! Callback for process launch attempt. + //! @param processId The id of the process that attempted to launch. + //! @param launchResult The result of the process launch attempt. + //! @param createTime The timestamp of the process launch attempt. + using ProcessLaunchCallback = + AZStd::function; + + //! Callback for process exit of successfully launched process. + //! @param processId The id of the process that attempted to launch. + //! @param exitStatus The circumstances upon which the processes exited. + //! @param returnCode The return code of the exited process. + //! @param std The standard output and standard error of the process. + //! @param createTime The timestamp of the process exit. + using ProcessExitCallback = + AZStd::function; + + //! Schedules a batch of processes for launch using a round robin approach to distribute the in-flight processes over + //! the specified number of concurrent process slots. + class ProcessScheduler + { + public: + //! Constructs the scheduler with the specified batch of processes. + //! @param maxConcurrentProcesses The maximum number of concurrent processes in-flight. + explicit ProcessScheduler(size_t maxConcurrentProcesses); + ~ProcessScheduler(); + + //! Executes the specified processes and calls the client callbacks (if any) as each process progresses in its life cycle. + //! @note Multiple subsequent calls to Execute are permitted. + //! @param processes The batch of processes to schedule. + //! @param processTimeout The maximum duration a process may be in-flight for before being forcefully terminated. + //! @param scheduleTimeout The maximum duration the scheduler may run before forcefully terminating all in-flight processes. + //! @param processLaunchCallback The process launch callback function. + //! @param processExitCallback The process exit callback function. + //! @returns The state that triggered the end of the schedule sequence. + ProcessSchedulerResult Execute( + const AZStd::vector& processes, + AZStd::optional processTimeout, + AZStd::optional scheduleTimeout, + ProcessLaunchCallback processLaunchCallback, + ProcessExitCallback processExitCallback); + + private: + class ExecutionState; + AZStd::unique_ptr m_executionState; + size_t m_maxConcurrentProcesses = 0; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.cpp new file mode 100644 index 0000000000..3edc5af28f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.cpp @@ -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. + * + */ + +#include +#include + +namespace TestImpact +{ + Process::Process(const ProcessInfo& processInfo) + : m_processInfo(processInfo) + { + } + + const ProcessInfo& Process::GetProcessInfo() const + { + return m_processInfo; + } + + AZStd::optional Process::GetReturnCode() const + { + return m_returnCode; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.h new file mode 100644 index 0000000000..763290d787 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcess.h @@ -0,0 +1,61 @@ +/* + * 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 + +namespace TestImpact +{ + //! Abstraction of platform-specific process. + class Process + { + public: + explicit Process(const ProcessInfo& processInfo); + virtual ~Process() = default; + + //! Terminates the process with the specified return code. + virtual void Terminate(ReturnCode returnCode) = 0; + + //! Block the calling thread until the process exits. + virtual void BlockUntilExit() = 0; + + //! Returns whether or not the process is still running. + virtual bool IsRunning() const = 0; + + //! Returns the process info associated with this process. + const ProcessInfo& GetProcessInfo() const; + + //! Returns the return code of the exited process. + //! Will be empty if the process is still running or was not successfully launched. + AZStd::optional GetReturnCode() const; + + //! Flushes the internal buffer and returns the process's buffered standard output. + //! Subsequent calls will keep returning data so long as the process is producing output. + //! Will return nullopt if no output routing or no output produced. + virtual AZStd::optional ConsumeStdOut() = 0; + + //! Flushes the internal buffer and returns the process's buffered standard error. + //! Subsequent calls will keep returning data so long as the process is producing errors. + //! Will return nullopt if no error routing or no errors produced. + virtual AZStd::optional ConsumeStdErr() = 0; + + protected: + //! The information used to launch the process. + ProcessInfo m_processInfo; + + //! The return code of a successfully launched process (otherwise is empty) + AZStd::optional m_returnCode; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessException.h new file mode 100644 index 0000000000..0c53800287 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessException.h @@ -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 + +#include + +namespace TestImpact +{ + //! Exception for processes and process-related operations. + class ProcessException + : public Exception + { + public: + using Exception::Exception; + }; +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.cpp new file mode 100644 index 0000000000..d8b876428e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.cpp @@ -0,0 +1,67 @@ +/* + * 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 + +namespace TestImpact +{ + ProcessInfo::ProcessInfo(ProcessId id, const RepoPath& processPath, const AZStd::string& startupArgs) + : m_id(id) + , m_parentHasStdOutput(false) + , m_parentHasStdErr(false) + , m_processPath(processPath) + , m_startupArgs(startupArgs) + { + AZ_TestImpact_Eval(processPath.String().length() > 0, ProcessException, "Process path cannot be empty"); + } + + ProcessInfo::ProcessInfo( + ProcessId id, + StdOutputRouting stdOut, + StdErrorRouting stdErr, + const RepoPath& processPath, + const AZStd::string& startupArgs) + : m_id(id) + , m_processPath(processPath) + , m_startupArgs(startupArgs) + , m_parentHasStdOutput(stdOut == StdOutputRouting::ToParent ? true : false) + , m_parentHasStdErr(stdErr == StdErrorRouting::ToParent ? true : false) + { + AZ_TestImpact_Eval(processPath.String().length() > 0, ProcessException, "Process path cannot be empty"); + } + + ProcessId ProcessInfo::GetId() const + { + return m_id; + } + + const RepoPath& ProcessInfo::GetProcessPath() const + { + return m_processPath; + } + + const AZStd::string& ProcessInfo::GetStartupArgs() const + { + return m_startupArgs; + } + + bool ProcessInfo::ParentHasStdOutput() const + { + return m_parentHasStdOutput; + } + + bool ProcessInfo::ParentHasStdError() const + { + return m_parentHasStdErr; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.h new file mode 100644 index 0000000000..f6d72d9ea8 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessInfo.h @@ -0,0 +1,94 @@ +/* + * 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 TestImpact +{ + //! Identifier to distinguish between processes. + using ProcessId = size_t; + + //! Return code of successfully launched process. + using ReturnCode = int; + + //! Error code for processes that are forcefully terminated whilst in-flight by the client. + inline constexpr const ReturnCode ProcessTerminateErrorCode = 0xF10BAD; + + //! Error code for processes that are forcefully terminated whilst in-flight by the scheduler due to timing out. + inline constexpr const ReturnCode ProcessTimeoutErrorCode = 0xBADF10; + + //! Specifier for how the process's standard out willt be routed + enum class StdOutputRouting + { + ToParent, + None + }; + + enum class StdErrorRouting + { + ToParent, + None + }; + + //! Container for process standard output and standard error. + struct StdContent + { + AZStd::optional m_out; + AZStd::optional m_err; + }; + + //! Information about a process the arguments used to launch it. + class ProcessInfo + { + public: + //! Provides the information required to launch a process. + //! @param processId Client-supplied id to diffrentiate between processes. + //! @param stdOut Routing of process standard output. + //! @param stdErr Routing of process standard error. + //! @param processPath Path to executable binary to launch. + //! @param startupArgs Arguments to launch the process with. + ProcessInfo( + ProcessId processId, + StdOutputRouting stdOut, + StdErrorRouting stdErr, + const RepoPath& processPath, + const AZStd::string& startupArgs = ""); + ProcessInfo(ProcessId processId, const RepoPath& processPath, const AZStd::string& startupArgs = ""); + + //! Returns the identifier of this process. + ProcessId GetId() const; + + //! Returns whether or not stdoutput is routed to the parent process. + bool ParentHasStdOutput() const; + + //! Returns whether or not stderror is routed to the parent process. + bool ParentHasStdError() const; + + // Returns the path to the process binary. + const RepoPath& GetProcessPath() const; + + //! Returns the command line arguments used to launch the process. + const AZStd::string& GetStartupArgs() const; + + private: + const ProcessId m_id; + const bool m_parentHasStdOutput; + const bool m_parentHasStdErr; + const RepoPath m_processPath; + const AZStd::string m_startupArgs; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessLauncher.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessLauncher.h new file mode 100644 index 0000000000..cefd16f003 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Process/TestImpactProcessLauncher.h @@ -0,0 +1,27 @@ +/* + * 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 + +namespace TestImpact +{ + class Process; + class ProcessInfo; + + //! Attempts to launch a process with the provided command line arguments. + //! @param processInfo The path and command line arguments to launch the process with. + AZStd::unique_ptr LaunchProcess(const ProcessInfo& processInfo); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.cpp new file mode 100644 index 0000000000..54161899ae --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.cpp @@ -0,0 +1,48 @@ +/* + * 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 "TestImpactBuildTarget.h" + +namespace TestImpact +{ + BuildTarget::BuildTarget(BuildTargetDescriptor&& descriptor, TargetType type) + : m_buildMetaData(AZStd::move(descriptor.m_buildMetaData)) + , m_sources(AZStd::move(descriptor.m_sources)) + , m_type(type) + { + } + + const AZStd::string& BuildTarget::GetName() const + { + return m_buildMetaData.m_name; + } + + const AZStd::string& BuildTarget::GetOutputName() const + { + return m_buildMetaData.m_outputName; + } + + const RepoPath& BuildTarget::GetPath() const + { + return m_buildMetaData.m_path; + } + + const TargetSources& BuildTarget::GetSources() const + { + return m_sources; + } + + TargetType BuildTarget::GetType() const + { + return m_type; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.h new file mode 100644 index 0000000000..d5d32b8ff5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTarget.h @@ -0,0 +1,65 @@ +/* + * 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 TestImpact +{ + class TestTarget; + class ProductionTarget; + + //! Holder for specializations of BuildTarget. + using Target = AZStd::variant; + + //! Optional holder for specializations of BuildTarget. + using OptionalTarget = AZStd::variant; + + //! Type id for querying specialized derived target types from base pointer/reference. + enum class TargetType : bool + { + Production, //!< Production build target. + Test //!< Test build target. + }; + + //! Representation of a generic build target in the repository. + class BuildTarget + { + public: + BuildTarget(BuildTargetDescriptor&& descriptor, TargetType type); + virtual ~BuildTarget() = default; + + //! Returns the build target name. + const AZStd::string& GetName() const; + + //! Returns the build target's compiled binary name. + const AZStd::string& GetOutputName() const; + + //! Returns the path in the source tree to the build target location. + const RepoPath& GetPath() const; + + //! Returns the build target's sources. + const TargetSources& GetSources() const; + + //! Returns the build target type. + TargetType GetType() const; + + private: + BuildMetaData m_buildMetaData; + TargetSources m_sources; + TargetType m_type; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTargetList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTargetList.h new file mode 100644 index 0000000000..79f93f7779 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactBuildTargetList.h @@ -0,0 +1,134 @@ +/* + * 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 TestImpact +{ + //! Container for unique set of sorted build target types. + //! @tparam Target The specialized build target type. + template + class BuildTargetList + { + public: + using TargetType = Target; + + BuildTargetList(AZStd::vector&& descriptors); + + //! Returns the targets in the collection. + const AZStd::vector& GetTargets() const; + + //! Returns the target with the specified name. + const Target* GetTarget(const AZStd::string& name) const; + + //! Returns the target with the specified name or throws if target not found. + const Target* GetTargetOrThrow(const AZStd::string& name) const; + + //! Returns true if the specified target is in the list, otherwise false. + bool HasTarget(const AZStd::string& name) const; + + // Returns the number of targets in the list. + size_t GetNumTargets() const; + + private: + AZStd::vector m_targets; + }; + + template + BuildTargetList::BuildTargetList(AZStd::vector&& descriptors) + { + AZ_TestImpact_Eval(!descriptors.empty(), TargetException, "Target list is empty"); + + AZStd::sort( + descriptors.begin(), descriptors.end(), [](const typename Target::Descriptor& lhs, const typename Target::Descriptor& rhs) + { + return lhs.m_buildMetaData.m_name < rhs.m_buildMetaData.m_name; + }); + + const auto duplicateElement = AZStd::adjacent_find( + descriptors.begin(), descriptors.end(), [](const typename Target::Descriptor& lhs, const typename Target::Descriptor& rhs) + { + return lhs.m_buildMetaData.m_name == rhs.m_buildMetaData.m_name; + }); + + AZ_TestImpact_Eval(duplicateElement == descriptors.end(), TargetException, "Target list contains duplicate targets"); + + m_targets.reserve(descriptors.size()); + for (auto&& descriptor : descriptors) + { + m_targets.emplace_back(Target(AZStd::move(descriptor))); + } + } + + template + const AZStd::vector& BuildTargetList::GetTargets() const + { + return m_targets; + } + + template + size_t BuildTargetList::GetNumTargets() const + { + return m_targets.size(); + } + + template + const Target* BuildTargetList::GetTarget(const AZStd::string& name) const + { + struct TargetComparator + { + bool operator()(const Target& target, const AZStd::string& name) const + { + return target.GetName() < name; + } + + bool operator()(const AZStd::string& name, const Target& target) const + { + return name < target.GetName(); + } + }; + + const auto targetRange = std::equal_range(m_targets.begin(), m_targets.end(), name, TargetComparator{}); + + if (targetRange.first != targetRange.second) + { + return targetRange.first; + } + else + { + return nullptr; + } + } + + template + const Target* BuildTargetList::GetTargetOrThrow(const AZStd::string& name) const + { + const Target* target = GetTarget(name); + AZ_TestImpact_Eval(target, TargetException, AZStd::string::format("Couldn't find target %s", name.c_str()).c_str()); + return target; + } + + template + bool BuildTargetList::HasTarget(const AZStd::string& name) const + { + return GetTarget(name) != nullptr; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.cpp new file mode 100644 index 0000000000..88c3cf932a --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.cpp @@ -0,0 +1,21 @@ +/* + * 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 "TestImpactProductionTarget.h" + +namespace TestImpact +{ + ProductionTarget::ProductionTarget(Descriptor&& descriptor) + : BuildTarget(AZStd::move(descriptor), TargetType::Production) + { + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.h new file mode 100644 index 0000000000..d4ddb6dc50 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTarget.h @@ -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. + * + */ + +#pragma once + +#include +#include + +namespace TestImpact +{ + //! Build target specialization for production targets (build targets containing production code and no test code). + class ProductionTarget + : public BuildTarget + { + public: + using Descriptor = ProductionTargetDescriptor; + ProductionTarget(Descriptor&& descriptor); + }; + + template + inline constexpr bool IsProductionTarget = AZStd::is_same_v>>>; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTargetList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTargetList.h new file mode 100644 index 0000000000..d2f23abe2e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactProductionTargetList.h @@ -0,0 +1,22 @@ +/* + * 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 + +namespace TestImpact +{ + //! Container for set of sorted production targets containing no duplicates. + using ProductionTargetList = BuildTargetList; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTargetException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTargetException.h new file mode 100644 index 0000000000..d3f2ec25ee --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTargetException.h @@ -0,0 +1,25 @@ +/* + * 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 TestImpact +{ + //! Exception for target and target-related operations. + class TargetException : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.cpp new file mode 100644 index 0000000000..1890863981 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.cpp @@ -0,0 +1,42 @@ +/* + * 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 "TestImpactTestTarget.h" + +namespace TestImpact +{ + TestTarget::TestTarget(Descriptor&& descriptor) + : BuildTarget(AZStd::move(descriptor), TargetType::Test) + , m_testMetaData(AZStd::move(descriptor.m_testMetaData)) + { + } + + const AZStd::string& TestTarget::GetSuite() const + { + return m_testMetaData.m_suite; + } + + const AZStd::string& TestTarget::GetCustomArgs() const + { + return m_testMetaData.m_customArgs; + } + + AZStd::chrono::milliseconds TestTarget::GetTimeout() const + { + return m_testMetaData.m_timeout; + } + + LaunchMethod TestTarget::GetLaunchMethod() const + { + return m_testMetaData.m_launchMethod; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.h new file mode 100644 index 0000000000..e8449dde3d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTarget.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 +#include + +namespace TestImpact +{ + //! Build target specialization for test targets (build targets containing test code and no production code). + class TestTarget + : public BuildTarget + { + public: + using Descriptor = TestTargetDescriptor; + + TestTarget(Descriptor&& descriptor); + + //! Returns the test target suite. + const AZStd::string& GetSuite() const; + + //! Returns the launcher custom arguments. + const AZStd::string& GetCustomArgs() const; + + //! Returns the test run timeout. + AZStd::chrono::milliseconds GetTimeout() const; + + //! Returns the test target launch method. + LaunchMethod GetLaunchMethod() const; + + private: + const TestTargetMeta m_testMetaData; + }; + + template + inline constexpr bool IsTestTarget = AZStd::is_same_v>>>; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTargetList.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTargetList.h new file mode 100644 index 0000000000..f8cafcb735 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/Target/TestImpactTestTargetList.h @@ -0,0 +1,22 @@ +/* + * 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 + +namespace TestImpact +{ + //! Container for set of sorted test targets containing no duplicates. + using TestTargetList = BuildTargetList; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumeration.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumeration.h new file mode 100644 index 0000000000..709c5d27d0 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumeration.h @@ -0,0 +1,22 @@ +/* + * 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 + +namespace TestImpact +{ + //! Representation of a given test target's enumerated tests. + using TestEnumeration = TestSuiteContainer; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.cpp new file mode 100644 index 0000000000..d1e7925cc0 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.cpp @@ -0,0 +1,100 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace TestImpact +{ + namespace TestEnumFields + { + // Keys for pertinent JSON node and attribute names + constexpr const char* Keys[] = + { + "suites", + "name", + "enabled", + "tests" + }; + + enum + { + SuitesKey, + NameKey, + EnabledKey, + TestsKey + }; + } // namespace + + AZStd::string SerializeTestEnumeration(const TestEnumeration& testEnum) + { + rapidjson::StringBuffer stringBuffer; + rapidjson::PrettyWriter writer(stringBuffer); + + writer.StartObject(); + writer.Key(TestEnumFields::Keys[TestEnumFields::SuitesKey]); + writer.StartArray(); + for (const auto& suite : testEnum.GetTestSuites()) + { + writer.StartObject(); + writer.Key(TestEnumFields::Keys[TestEnumFields::NameKey]); + writer.String(suite.m_name.c_str()); + writer.Key(TestEnumFields::Keys[TestEnumFields::EnabledKey]); + writer.Bool(suite.m_enabled); + writer.Key(TestEnumFields::Keys[TestEnumFields::TestsKey]); + writer.StartArray(); + for (const auto& test : suite.m_tests) + { + writer.StartObject(); + writer.Key(TestEnumFields::Keys[TestEnumFields::NameKey]); + writer.String(test.m_name.c_str()); + writer.Key(TestEnumFields::Keys[TestEnumFields::EnabledKey]); + writer.Bool(test.m_enabled); + writer.EndObject(); + } + writer.EndArray(); + writer.EndObject(); + } + writer.EndArray(); + writer.EndObject(); + + return stringBuffer.GetString(); + } + + TestEnumeration DeserializeTestEnumeration(const AZStd::string& testEnumString) + { + AZStd::vector testSuites; + rapidjson::Document doc; + + if (doc.Parse<0>(testEnumString.c_str()).HasParseError()) + { + throw TestEngineException("Could not parse enumeration data"); + } + + for (const auto& suite : doc[TestEnumFields::Keys[TestEnumFields::SuitesKey]].GetArray()) + { + testSuites.emplace_back(TestEnumerationSuite{suite[TestEnumFields::Keys[TestEnumFields::NameKey]].GetString(), suite[TestEnumFields::Keys[TestEnumFields::EnabledKey]].GetBool(), {}}); + for (const auto& test : suite[TestEnumFields::Keys[TestEnumFields::TestsKey]].GetArray()) + { + testSuites.back().m_tests.emplace_back( + TestEnumerationCase{test[TestEnumFields::Keys[TestEnumFields::NameKey]].GetString(), test[TestEnumFields::Keys[TestEnumFields::EnabledKey]].GetBool()}); + } + } + + return TestEnumeration(std::move(testSuites)); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h new file mode 100644 index 0000000000..06bd224b50 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h @@ -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 + +#include + +#include + +namespace TestImpact +{ + //! Serializes the specified test enumeration to JSON format. + AZStd::string SerializeTestEnumeration(const TestEnumeration& testEnumeration); + + //! Deserializes a test enumeration from the specified test enumeration data in JSON format. + TestEnumeration DeserializeTestEnumeration(const AZStd::string& testEnumerationString); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.cpp new file mode 100644 index 0000000000..17784dd937 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.cpp @@ -0,0 +1,206 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + TestEnumeration ParseTestEnumerationFile(const RepoPath& enumerationFile) + { + return TestEnumeration(GTest::TestEnumerationSuitesFactory(ReadFileContents(enumerationFile))); + } + + TestEnumerationJobData::TestEnumerationJobData(const RepoPath& enumerationArtifact, AZStd::optional&& cache) + : m_enumerationArtifact(enumerationArtifact) + , m_cache(AZStd::move(cache)) + { + } + + const RepoPath& TestEnumerationJobData::GetEnumerationArtifactPath() const + { + return m_enumerationArtifact; + } + + const AZStd::optional& TestEnumerationJobData::GetCache() const + { + return m_cache; + } + + TestEnumerator::TestEnumerator(size_t maxConcurrentEnumerations) + : JobRunner(maxConcurrentEnumerations) + { + } + + AZStd::pair> TestEnumerator::Enumerate( + const AZStd::vector& jobInfos, + AZStd::optional enumerationTimeout, + AZStd::optional enumeratorTimeout, + AZStd::optional clientCallback) + { + AZStd::vector cachedJobs; + AZStd::vector jobQueue; + + for (auto jobInfo = jobInfos.begin(); jobInfo != jobInfos.end(); ++jobInfo) + { + // If this job has a cache read policy attempt to read the cache + if (jobInfo->GetCache().has_value()) + { + if (jobInfo->GetCache()->m_policy == JobData::CachePolicy::Read) + { + JobMeta meta; + AZStd::optional enumeration; + + try + { + enumeration = TestEnumeration(DeserializeTestEnumeration(ReadFileContents(jobInfo->GetCache()->m_file))); + } + catch (const TestEngineException& e) + { + AZ_Printf("Enumerate", AZStd::string::format("Enumeration cache error: %s\n", e.what()).c_str()); + DeleteFile(jobInfo->GetCache()->m_file); + } + + // Even though cached jobs don't get executed we still give the client the opportunity to handle the job state + // change in order to make the caching process transparent to the client + if (enumeration.has_value()) + { + // Cache read successfully, this job will not be placed in the job queue + cachedJobs.emplace_back(Job(*jobInfo, AZStd::move(meta), AZStd::move(enumeration))); + + if (m_clientJobCallback.has_value() && (*m_clientJobCallback)(*jobInfo, meta) == ProcessCallbackResult::Abort) + { + // Client chose to abort so we will copy over the existing cache enumerations and fill the rest with blanks + AZStd::vector jobs(cachedJobs); + for (auto emptyJobInfo = ++jobInfo; emptyJobInfo != jobInfos.end(); ++emptyJobInfo) + { + jobs.emplace_back(Job(*emptyJobInfo, {}, AZStd::nullopt)); + } + + return { ProcessSchedulerResult::UserAborted, jobs }; + } + } + else + { + // The cache read failed and exception policy for cache read failures is not to throw so instead place this + // job in the job queue + jobQueue.emplace_back(*jobInfo); + } + } + else + { + // This job has no cache read policy so delete the cache and place in job queue + DeleteFile(jobInfo->GetCache()->m_file); + jobQueue.emplace_back(*jobInfo); + } + } + else + { + // This job has no cache read policy so delete the cache and place in job queue + DeleteFile(jobInfo->GetCache()->m_file); + jobQueue.emplace_back(*jobInfo); + } + } + + /* As per comment on PR51, this suggestion will be explored once the test coverage code for this subsystem is revisited + bool aborted = false; + for (const auto& jobInfo : jobInfos) + { + if (!jobInfo->GetCache().has_value() || + jobInfo->GetCache()->m_policy != JobData::CachePolicy::Read) + { + DeleteFile(jobInfo->GetCache()->m_file); + jobQueue.emplace_back(*jobInfo); + continue; + } + + ... // try catch part + if (!enumeration.has_value()) + { + jobQueue.emplace_back(*jobInfo); + continue; + } + + // Cache read successfully, this job will not be placed in the job queue + cachedJobs.emplace_back(Job(*jobInfo, AZStd::move(meta), AZStd::move(enumeration))); + + if (m_clientJobCallback.has_value() && (*m_clientJobCallback)(*jobInfo, meta) == ProcessCallbackResult::Abort) + { + aborted = true; // catch the index too + break; + } + } + + if (aborted) + { + // do the abortion part + + return { ProcessSchedulerResult::UserAborted, jobs }; + } + */ + + const auto payloadGenerator = [this](const JobDataMap& jobDataMap) + { + PayloadMap enumerations; + for (const auto& [jobId, jobData] : jobDataMap) + { + const auto& [meta, jobInfo] = jobData; + if (meta.m_result == JobResult::ExecutedWithSuccess) + { + try + { + const auto& enumeration = (enumerations[jobId] = ParseTestEnumerationFile(jobInfo->GetEnumerationArtifactPath())); + + // Write out the enumeration to a cache file if we have a cache write policy for this job + if (jobInfo->GetCache().has_value() && jobInfo->GetCache()->m_policy == JobData::CachePolicy::Write) + { + WriteFileContents(SerializeTestEnumeration(enumeration.value()), jobInfo->GetCache()->m_file); + } + } + catch (const Exception& e) + { + AZ_Warning("Enumerate", false, e.what()); + enumerations[jobId] = AZStd::nullopt; + } + } + } + + return enumerations; + }; + + // Generate the enumeration results for the jobs that weren't cached + auto [result, jobs] = ExecuteJobs( + jobQueue, + payloadGenerator, + StdOutputRouting::None, + StdErrorRouting::None, + enumerationTimeout, + enumeratorTimeout, + clientCallback, + AZStd::nullopt); + + // We need to add the cached jobs to the completed job list even though they technically weren't executed + for (auto&& job : cachedJobs) + { + jobs.emplace_back(AZStd::move(job)); + } + + return { result, jobs }; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.h new file mode 100644 index 0000000000..196700998f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Enumeration/TestImpactTestEnumerator.h @@ -0,0 +1,79 @@ +/* + * 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 + +namespace TestImpact +{ + //! Per-job data for test enumerations. + class TestEnumerationJobData + { + public: + //! Policy for how a test enumeration will be written/read from the a previous cache instead of enumerated from the test target. + enum class CachePolicy + { + Read, //!< Do read from a cache file but do not overwrite any existing cache file. + Write //!< Do not read from a cache file but instead overwrite any existing cache file. + }; + + //! Cache configuration for a given test enumeration command. + struct Cache + { + CachePolicy m_policy; + RepoPath m_file; + }; + + TestEnumerationJobData(const RepoPath& enumerationArtifact, AZStd::optional&& cache); + + //! Returns the path to the enumeration artifact produced by the test target. + const RepoPath& GetEnumerationArtifactPath() const; + + //! Returns the cache details for this job. + const AZStd::optional& GetCache() const; + + private: + RepoPath m_enumerationArtifact; //!< Path to enumeration artifact to be processed. + AZStd::optional m_cache = AZStd::nullopt; //!< No caching takes place if cache is empty. + }; + + //! Enumerate a batch of test targets to determine the test suites and fixtures they contain, caching the results where applicable. + class TestEnumerator + : public TestJobRunner + { + using JobRunner = TestJobRunner; + + public: + //! Constructs a test enumerator with the specified parameters common to all enumeration job runs of this enumerator. + //! @param maxConcurrentEnumerations The maximum number of enumerations to be in flight at any given time. + explicit TestEnumerator(size_t maxConcurrentEnumerations); + + //! Executes the specified test enumeration jobs according to the specified cache and job exception policies. + //! @param jobInfos The enumeration jobs to execute. + //! @param cacheExceptionPolicy The cache exception policy to be used for this run. + //! @param jobExceptionPolicy The enumeration job exception policy to be used for this run. + //! @param enumerationTimeout The maximum duration an enumeration may be in-flight for before being forcefully terminated. + //! @param enumeratorTimeout The maximum duration the enumerator may run before forcefully terminating all in-flight enumerations. + //! @param clientCallback The optional client callback to be called whenever an enumeration job changes state. + //! @return The result of the run sequence and the enumeration jobs with their associated test enumeration payloads. + AZStd::pair> Enumerate( + const AZStd::vector& jobInfos, + AZStd::optional enumerationTimeout, + AZStd::optional enumeratorTimeout, + AZStd::optional clientCallback); + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.cpp new file mode 100644 index 0000000000..356688a128 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.cpp @@ -0,0 +1,194 @@ +/* + * 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 TestImpact +{ + TestJobInfoGenerator::TestJobInfoGenerator( + const RepoPath& sourceDir, + const RepoPath& targetBinaryDir, + const RepoPath& cacheDir, + const RepoPath& artifactDir, + const RepoPath& testRunnerBinary, + const RepoPath& instrumentBinary) + : m_sourceDir(sourceDir) + , m_targetBinaryDir(targetBinaryDir) + , m_cacheDir(cacheDir) + , m_artifactDir(artifactDir) + , m_testRunnerBinary(testRunnerBinary) + , m_instrumentBinary(instrumentBinary) + { + } + + AZStd::string TestJobInfoGenerator::GenerateLaunchArgument(const TestTarget* testTarget) const + { + if (testTarget->GetLaunchMethod() == LaunchMethod::StandAlone) + { + return AZStd::string::format( + "%s%s %s", + (m_targetBinaryDir / RepoPath(testTarget->GetOutputName())).c_str(), + GetTestTargetExtension(testTarget).c_str(), + testTarget->GetCustomArgs().c_str()).c_str(); + } + else + { + return AZStd::string::format( + "\"%s\" \"%s%s\" %s", + m_testRunnerBinary.c_str(), + (m_targetBinaryDir / RepoPath(testTarget->GetOutputName())).c_str(), + GetTestTargetExtension(testTarget).c_str(), + testTarget->GetCustomArgs().c_str()).c_str(); + } + } + + RepoPath TestJobInfoGenerator::GenerateTargetEnumerationCacheFilePath(const TestTarget* testTarget) const + { + return AZStd::string::format("%s.cache", (m_cacheDir / RepoPath(testTarget->GetName())).c_str()); + } + + RepoPath TestJobInfoGenerator::GenerateTargetEnumerationArtifactFilePath(const TestTarget* testTarget) const + { + return AZStd::string::format("%s.Enumeration.xml", (m_artifactDir / RepoPath(testTarget->GetName())).c_str()); + } + + RepoPath TestJobInfoGenerator::GenerateTargetRunArtifactFilePath(const TestTarget* testTarget) const + { + return AZStd::string::format("%s.Run.xml", (m_artifactDir / RepoPath(testTarget->GetName())).c_str()); + } + + RepoPath TestJobInfoGenerator::GenerateTargetCoverageArtifactFilePath(const TestTarget* testTarget) const + { + return AZStd::string::format("%s.Coverage.xml", (m_artifactDir / RepoPath(testTarget->GetName())).c_str()); + } + + TestEnumerator::JobInfo TestJobInfoGenerator::GenerateTestEnumerationJobInfo( + const TestTarget* testTarget, + TestEnumerator::JobInfo::Id jobId, + TestEnumerator::JobInfo::CachePolicy cachePolicy) const + { + using Command = TestEnumerator::Command; + using JobInfo = TestEnumerator::JobInfo; + using JobData = TestEnumerator::JobData; + using Cache = TestEnumerator::JobData::Cache; + + const auto enumerationArtifact = GenerateTargetEnumerationArtifactFilePath(testTarget); + const Command args = + { + AZStd::string::format( + "%s --gtest_list_tests --gtest_output=xml:\"%s\"", + GenerateLaunchArgument(testTarget).c_str(), + enumerationArtifact.c_str()) + }; + + return JobInfo(jobId, args, JobData(enumerationArtifact, Cache{ cachePolicy, GenerateTargetEnumerationCacheFilePath(testTarget) })); + } + + TestRunner::JobInfo TestJobInfoGenerator::GenerateRegularTestRunJobInfo( + const TestTarget* testTarget, + TestRunner::JobInfo::Id jobId) const + { + using Command = TestRunner::Command; + using JobInfo = TestRunner::JobInfo; + using JobData = TestRunner::JobData; + + const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget); + const Command args = + { + AZStd::string::format( + "%s --gtest_output=xml:\"%s\"", + GenerateLaunchArgument(testTarget).c_str(), + runArtifact.c_str()) + }; + + return JobInfo(jobId, args, JobData(runArtifact)); + } + + InstrumentedTestRunner::JobInfo TestJobInfoGenerator::GenerateInstrumentedTestRunJobInfo( + const TestTarget* testTarget, + InstrumentedTestRunner::JobInfo::Id jobId, + CoverageLevel coverageLevel) const + { + using Command = InstrumentedTestRunner::Command; + using JobInfo = InstrumentedTestRunner::JobInfo; + using JobData = InstrumentedTestRunner::JobData; + + const auto coverageArtifact = GenerateTargetCoverageArtifactFilePath(testTarget); + const auto runArtifact = GenerateTargetRunArtifactFilePath(testTarget); + const Command args = + { + AZStd::string::format( + "\"%s\" " // 1. Instrumented test runner + "--coverage_level %s " // 2. Coverage level + "--export_type cobertura:\"%s\" " // 3. Test coverage artifact path + "--modules \"%s\" " // 4. Modules path + "--excluded_modules \"%s\" " // 5. Exclude modules + "--sources \"%s\" -- " // 6. Sources path + "%s " // 7. Launch command + "--gtest_output=xml:\"%s\"", // 8. Result artifact + + m_instrumentBinary.c_str(), // 1. Instrumented test runner + (coverageLevel == CoverageLevel::Line ? "line" : "source"), // 2. Coverage level + coverageArtifact.c_str(), // 3. Test coverage artifact path + m_targetBinaryDir.c_str(), // 4. Modules path + m_testRunnerBinary.c_str(), // 5. Exclude modules + m_sourceDir.c_str(), // 6. Sources path + GenerateLaunchArgument(testTarget).c_str(), // 7. Launch command + runArtifact.c_str()) // 8. Result artifact + }; + + return JobInfo(jobId, args, JobData(runArtifact, coverageArtifact)); + } + + AZStd::vector TestJobInfoGenerator::GenerateTestEnumerationJobInfos( + const AZStd::vector& testTargets, + TestEnumerator::JobInfo::CachePolicy cachePolicy) const + { + AZStd::vector jobInfos; + jobInfos.reserve(testTargets.size()); + for (size_t jobId = 0; jobId < testTargets.size(); jobId++) + { + jobInfos.push_back(GenerateTestEnumerationJobInfo(testTargets[jobId], { jobId }, cachePolicy)); + } + + return jobInfos; + } + + AZStd::vector TestJobInfoGenerator::GenerateRegularTestRunJobInfos( + const AZStd::vector& testTargets) const + { + AZStd::vector jobInfos; + jobInfos.reserve(testTargets.size()); + for (size_t jobId = 0; jobId < testTargets.size(); jobId++) + { + jobInfos.push_back(GenerateRegularTestRunJobInfo(testTargets[jobId], { jobId })); + } + + return jobInfos; + } + + AZStd::vector TestJobInfoGenerator::GenerateInstrumentedTestRunJobInfos( + const AZStd::vector& testTargets, + CoverageLevel coverageLevel) const + { + AZStd::vector jobInfos; + jobInfos.reserve(testTargets.size()); + for (size_t jobId = 0; jobId < testTargets.size(); jobId++) + { + jobInfos.push_back(GenerateInstrumentedTestRunJobInfo(testTargets[jobId], { jobId }, coverageLevel)); + } + + return jobInfos; + } +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.h new file mode 100644 index 0000000000..1bd06f6372 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.h @@ -0,0 +1,108 @@ +/* + * 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 TestImpact +{ + class TestTarget; + + //! Generates job information for the different test job runner types. + class TestJobInfoGenerator + { + public: + //! Configures the test job info generator with the necessary path information for launching test targets. + //! @param sourceDir Root path where source files are found (including subfolders). + //! @param targetBinaryDir Path to where the test target binaries are found. + //! @param cacheDir Path to the persistent folder where test target enumerations are cached. + //! @param artifactDir Path to the transient directory where test artifacts are produced. + //! @param testRunnerBinary Path to the binary responsible for launching test targets that have the TestRunner launch method. + //! @param instrumentBinary Path to the binary responsible for launching test targets with test coverage instrumentation. + TestJobInfoGenerator( + const RepoPath& sourceDir, + const RepoPath& targetBinaryDir, + const RepoPath& cacheDir, + const RepoPath& artifactDir, + const RepoPath& testRunnerBinary, + const RepoPath& instrumentBinary); + + //! Generates the information for a test enumeration job. + //! @param testTarget The test target to generate the job information for. + //! @param jobId The id to assign for this job. + //! @param cachePolicy The cache policy to use for this job. + TestEnumerator::JobInfo GenerateTestEnumerationJobInfo( + const TestTarget* testTarget, + TestEnumerator::JobInfo::Id jobId, + TestEnumerator::JobInfo::CachePolicy cachePolicy) const; + + //! Generates the information for a test run job. + //! @param testTarget The test target to generate the job information for. + //! @param jobId The id to assign for this job. + TestRunner::JobInfo GenerateRegularTestRunJobInfo( + const TestTarget* testTarget, + TestRunner::JobInfo::Id jobId) const; + + //! Generates the information for an instrumented test run job. + //! @param testTarget The test target to generate the job information for. + //! @param jobId The id to assign for this job. + //! @param coverageLevel The coverage level to use for this job. + InstrumentedTestRunner::JobInfo GenerateInstrumentedTestRunJobInfo( + const TestTarget* testTarget, + InstrumentedTestRunner::JobInfo::Id jobId, + CoverageLevel coverageLevel) const; + + //! Generates the information for the batch of test enumeration jobs. + AZStd::vector GenerateTestEnumerationJobInfos( + const AZStd::vector& testTargets, + TestEnumerator::JobInfo::CachePolicy cachePolicy) const; + + //! Generates the information for the batch of test run jobs. + AZStd::vector GenerateRegularTestRunJobInfos( + const AZStd::vector& testTargets) const; + + //! Generates the information for the batch of instrumented test run jobs. + AZStd::vector GenerateInstrumentedTestRunJobInfos( + const AZStd::vector& testTargets, + CoverageLevel coverageLevel) const; + private: + //! Generates the command string to launch the specified test target. + AZStd::string GenerateLaunchArgument(const TestTarget* testTarget) const; + + //! Generates the path to the enumeration cache file for the specified test target. + RepoPath GenerateTargetEnumerationCacheFilePath(const TestTarget* testTarget) const; + + //! Generates the path to the enumeration artifact file for the specified test target. + RepoPath GenerateTargetEnumerationArtifactFilePath(const TestTarget* testTarget) const; + + //! Generates the path to the test run artifact file for the specified test target. + RepoPath GenerateTargetRunArtifactFilePath(const TestTarget* testTarget) const; + + //! Generates the path to the test coverage artifact file for the specified test target. + RepoPath GenerateTargetCoverageArtifactFilePath(const TestTarget* testTarget) const; + + RepoPath m_sourceDir; + RepoPath m_targetBinaryDir; + RepoPath m_cacheDir; + RepoPath m_artifactDir; + RepoPath m_testRunnerBinary; + RepoPath m_instrumentBinary; + }; +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobRunner.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobRunner.h new file mode 100644 index 0000000000..0c1c8189e3 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestJobRunner.h @@ -0,0 +1,114 @@ +/* + * 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 TestImpact +{ + //! Base class for test related job runners. + //! @tparam AdditionalInfo The data structure containing the information additional to the command arguments necessary to execute and + //! complete a job. + //! @tparam Payload The output produced by a job. + template + class TestJobRunner + { + public: + using JobData = AdditionalInfo; + using JobInfo = JobInfo; + using Command = typename JobInfo::Command; + using JobPayload = Payload; + using Job = Job; + using ClientJobCallback = AZStd::function; + using DerivedJobCallback = JobCallback; + using JobDataMap = JobDataMap; + + //! Constructs the job runner with the specified parameters common to all job runs of this runner. + //! @param maxConcurrentJobs The maximum number of jobs to be in flight at any given time. + explicit TestJobRunner(size_t maxConcurrentJobs); + + protected: + //! Runs the specified jobs and returns the completed payloads produced by each job. + //! @param jobInfos The batch of jobs to execute. + //! @param jobExceptionPolicy The job execution policy for this job run. + //! @param payloadMapProducer The client callback for producing the payload map based on the completed job data. + //! @param stdOutRouting The standard output routing from the underlying job processes to the derived runner. + //! @param stdErrorRouting The standard error routing from the underlying job processes to the derived runner. + //! @param jobTimeout The maximum duration a job may be in-flight for before being forcefully terminated (nullopt if no timeout). + //! @param runnerTimeout The maximum duration the runner may run before forcefully terminating all in-flight jobs (nullopt if no timeout). + //! @param clientCallback The optional callback function provided by the client to be called upon job state change. + //! @param clientCallback The optional callback function provided by the derived job runner to be called upon job state change. + //! @returns The result of the run sequence and the jobs that the sequence produced. + AZStd::pair> ExecuteJobs( + const AZStd::vector& jobInfos, + PayloadMapProducer payloadMapProducer, + StdOutputRouting stdOutRouting, + StdErrorRouting stdErrRouting, + AZStd::optional jobTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback, + AZStd::optional derivedJobCallback); + + const AZStd::optional m_clientJobCallback; + + private: + JobRunner m_jobRunner; + const AZStd::optional m_derivedJobCallback; + }; + + template + TestJobRunner::TestJobRunner(size_t maxConcurrentJobs) + : m_jobRunner(maxConcurrentJobs) + { + } + + template + AZStd::pair::Job>> TestJobRunner::ExecuteJobs( + const AZStd::vector& jobInfos, + PayloadMapProducer payloadMapProducer, + StdOutputRouting stdOutRouting, + StdErrorRouting stdErrRouting, + AZStd::optional jobTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback, + AZStd::optional derivedJobCallback) + { + // Callback to handle job exception policies and client/derived callbacks + const auto jobCallback = [&clientCallback, &derivedJobCallback](const JobInfo& jobInfo, const JobMeta& meta, StdContent&& std) + { + auto callbackResult = ProcessCallbackResult::Continue; + if (derivedJobCallback.has_value()) + { + callbackResult = (*derivedJobCallback)(jobInfo, meta, AZStd::move(std)); + } + + if (clientCallback.has_value()) + { + if (const auto result = (*clientCallback)(jobInfo, meta); + result == ProcessCallbackResult::Abort) + { + callbackResult = ProcessCallbackResult::Abort; + } + } + + return callbackResult; + }; + + return m_jobRunner.Execute(jobInfos, payloadMapProducer, stdOutRouting, stdErrRouting, jobTimeout, runnerTimeout, jobCallback); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestTargetExtension.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestTargetExtension.h new file mode 100644 index 0000000000..9251164052 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/JobRunner/TestImpactTestTargetExtension.h @@ -0,0 +1,23 @@ +/* + * 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 TestImpact +{ + class TestTarget; + + //! Returns the binary file extension for the specified test target. + AZStd::string GetTestTargetExtension(const TestTarget* testTarget); +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.cpp new file mode 100644 index 0000000000..5a0ef74472 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.cpp @@ -0,0 +1,94 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + InstrumentedTestRunJobData::InstrumentedTestRunJobData(const RepoPath& resultsArtifact, const RepoPath& coverageArtifact) + : TestRunJobData(resultsArtifact) + , m_coverageArtifact(coverageArtifact) + { + } + + const RepoPath& InstrumentedTestRunJobData::GetCoverageArtifactPath() const + { + return m_coverageArtifact; + } + + InstrumentedTestRunner::JobPayload ParseTestRunAndCoverageFiles( + const RepoPath& runFile, + const RepoPath& coverageFile, + AZStd::chrono::milliseconds duration) + { + TestRun run(GTest::TestRunSuitesFactory(ReadFileContents(runFile)), duration); + AZStd::vector moduleCoverages = Cobertura::ModuleCoveragesFactory(ReadFileContents(coverageFile)); + TestCoverage coverage(AZStd::move(moduleCoverages)); + return {AZStd::move(run), AZStd::move(coverage)}; + } + + InstrumentedTestRunner::InstrumentedTestRunner(size_t maxConcurrentRuns) + : JobRunner(maxConcurrentRuns) + { + } + + AZStd::pair> InstrumentedTestRunner::RunInstrumentedTests( + const AZStd::vector& jobInfos, + AZStd::optional runTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback) + { + const auto payloadGenerator = [this](const JobDataMap& jobDataMap) + { + PayloadMap runs; + for (const auto& [jobId, jobData] : jobDataMap) + { + const auto& [meta, jobInfo] = jobData; + if (meta.m_result == JobResult::ExecutedWithSuccess || meta.m_result == JobResult::ExecutedWithFailure) + { + try + { + runs[jobId] = ParseTestRunAndCoverageFiles( + jobInfo->GetRunArtifactPath(), + jobInfo->GetCoverageArtifactPath(), + meta.m_duration.value()); + } + catch (const Exception& e) + { + AZ_Printf("RunInstrumentedTests", AZStd::string::format("%s\n", e.what()).c_str()); + runs[jobId] = AZStd::nullopt; + } + } + } + + return runs; + }; + + return ExecuteJobs( + jobInfos, + payloadGenerator, + StdOutputRouting::None, + StdErrorRouting::None, + runTimeout, + runnerTimeout, + clientCallback, + AZStd::nullopt); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.h new file mode 100644 index 0000000000..2f3e6eb98e --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactInstrumentedTestRunner.h @@ -0,0 +1,61 @@ +/* + * 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 + +namespace TestImpact +{ + //! Per-job data for instrumented test runs. + class InstrumentedTestRunJobData + : public TestRunJobData + { + public: + InstrumentedTestRunJobData(const RepoPath& resultsArtifact, const RepoPath& coverageArtifact); + + //! Returns the path to the coverage artifact produced by the test target. + const RepoPath& GetCoverageArtifactPath() const; + + private: + RepoPath m_coverageArtifact; //!< Path to coverage data. + }; + + //! Runs a batch of test targets to determine the test coverage and passes/failures. + class InstrumentedTestRunner + : public TestJobRunner> + { + using JobRunner = TestJobRunner>; + + public: + //! Constructs an instrumented test runner with the specified parameters common to all job runs of this runner. + //! @param maxConcurrentRuns The maximum number of runs to be in flight at any given time. + explicit InstrumentedTestRunner(size_t maxConcurrentRuns); + + //! Executes the specified instrumented test run jobs according to the specified job exception policies. + //! @param jobInfos The test run jobs to execute. + //! @param jobExceptionPolicy The test run job exception policy to be used for this run (use + //! TestJobExceptionPolicy::OnFailedToExecute to throw on test failures). + //! @param runTimeout The maximum duration a run may be in-flight for before being forcefully terminated. + //! @param runnerTimeout The maximum duration the runner may run before forcefully terminating all in-flight runs. + //! @param clientCallback The optional client callback to be called whenever a run job changes state. + //! @return The result of the run sequence and the instrumented run jobs with their associated test run and coverage payloads. + AZStd::pair> RunInstrumentedTests( + const AZStd::vector& jobInfos, + AZStd::optional runTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback); + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.cpp new file mode 100644 index 0000000000..4e82857cb2 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.cpp @@ -0,0 +1,120 @@ +/* + * 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 TestImpact +{ + TestCoverage::TestCoverage(const TestCoverage& other) + : m_modules(other.m_modules) + , m_sourcesCovered(other.m_sourcesCovered) + , m_coverageLevel(other.m_coverageLevel) + { + } + + TestCoverage::TestCoverage(TestCoverage&& other) noexcept + : m_modules(AZStd::move(other.m_modules)) + , m_sourcesCovered(AZStd::move(other.m_sourcesCovered)) + { + AZStd::swap(m_coverageLevel, other.m_coverageLevel); + } + + TestCoverage::TestCoverage(const AZStd::vector& moduleCoverages) + : m_modules(moduleCoverages) + { + CalculateTestMetrics(); + } + + TestCoverage::TestCoverage(AZStd::vector&& moduleCoverages) noexcept + : m_modules(AZStd::move(moduleCoverages)) + { + CalculateTestMetrics(); + } + + TestCoverage& TestCoverage::operator=(const TestCoverage& other) + { + if (this != &other) + { + m_modules = other.m_modules; + m_sourcesCovered = other.m_sourcesCovered; + m_coverageLevel = other.m_coverageLevel; + } + + return *this; + } + + TestCoverage& TestCoverage::operator=(TestCoverage&& other) noexcept + { + if (this != &other) + { + m_modules = AZStd::move(other.m_modules); + m_sourcesCovered = other.m_sourcesCovered; + m_coverageLevel = other.m_coverageLevel; + } + + return *this; + } + + void TestCoverage::CalculateTestMetrics() + { + m_coverageLevel.reset(); + m_sourcesCovered.clear(); + + for (const auto& moduleCovered : m_modules) + { + for (const auto& sourceCovered : moduleCovered.m_sources) + { + m_sourcesCovered.emplace_back(sourceCovered.m_path); + if (!sourceCovered.m_coverage.empty()) + { + m_coverageLevel = CoverageLevel::Line; + } + } + } + + AZStd::sort(m_sourcesCovered.begin(), m_sourcesCovered.end()); + m_sourcesCovered.erase(AZStd::unique(m_sourcesCovered.begin(), m_sourcesCovered.end()), m_sourcesCovered.end()); + + if (!m_coverageLevel.has_value() && !m_sourcesCovered.empty()) + { + m_coverageLevel = CoverageLevel::Source; + } + } + + size_t TestCoverage::GetNumSourcesCovered() const + { + return m_sourcesCovered.size(); + } + + size_t TestCoverage::GetNumModulesCovered() const + { + return m_modules.size(); + } + + const AZStd::vector& TestCoverage::GetSourcesCovered() const + { + return m_sourcesCovered; + } + + const AZStd::vector& TestCoverage::GetModuleCoverages() const + { + return m_modules; + } + + AZStd::optional TestCoverage::GetCoverageLevel() const + { + return m_coverageLevel; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.h new file mode 100644 index 0000000000..8de5fbb6c7 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestCoverage.h @@ -0,0 +1,62 @@ +/* + * 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 + +namespace TestImpact +{ + //! Scope of coverage data. + enum class CoverageLevel : bool + { + Source, //!< Line-level coverage data. + Line //!< Source-level coverage data. + }; + + //! Representation of a given test target's test coverage results. + class TestCoverage + { + public: + TestCoverage(const TestCoverage&); + TestCoverage(TestCoverage&&) noexcept; + TestCoverage(AZStd::vector&& moduleCoverages) noexcept; + TestCoverage(const AZStd::vector& moduleCoverages); + + TestCoverage& operator=(const TestCoverage&); + TestCoverage& operator=(TestCoverage&&) noexcept; + + //! Returns the number of unique sources covered. + size_t GetNumSourcesCovered() const; + + //! Returns the number of modules (dynamic libraries, child processes, etc.) covered. + size_t GetNumModulesCovered() const; + + //! Returns the sorted set of unique sources covered (empty if no coverage). + const AZStd::vector& GetSourcesCovered() const; + + //! Returns the modules covered (empty if no coverage). + const AZStd::vector& GetModuleCoverages() const; + + //! Returns the coverage level (empty if no coverage). + AZStd::optional GetCoverageLevel() const; + + private: + void CalculateTestMetrics(); + + AZStd::vector m_modules; + AZStd::vector m_sourcesCovered; + AZStd::optional m_coverageLevel; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.cpp new file mode 100644 index 0000000000..6a361ca9d0 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.cpp @@ -0,0 +1,138 @@ +/* + * 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 "TestImpactTestRun.h" + +namespace TestImpact +{ + TestRun::TestRun(const TestRun& other) + : TestSuiteContainer(other) + , m_numRuns(other.m_numRuns) + , m_numNotRuns(other.m_numNotRuns) + , m_numPasses(other.m_numPasses) + , m_numFailures(other.m_numFailures) + , m_duration(other.m_duration) + { + CalculateTestMetrics(); + } + + TestRun::TestRun(TestRun&& other) noexcept + : TestSuiteContainer(AZStd::move(other)) + , m_numRuns(other.m_numRuns) + , m_numNotRuns(other.m_numNotRuns) + , m_numPasses(other.m_numPasses) + , m_numFailures(other.m_numFailures) + , m_duration(other.m_duration) + { + } + + TestRun::TestRun(AZStd::vector&& testSuites, AZStd::chrono::milliseconds duration) noexcept + : TestSuiteContainer(AZStd::move(testSuites)) + , m_duration(duration) + { + CalculateTestMetrics(); + } + + TestRun::TestRun(const AZStd::vector& testSuites, AZStd::chrono::milliseconds duration) + : TestSuiteContainer(testSuites) + , m_duration(duration) + { + CalculateTestMetrics(); + } + + TestRun& TestRun::operator=(TestRun&& other) noexcept + { + if (this != &other) + { + TestSuiteContainer::operator=(AZStd::move(other)); + m_numRuns = other.m_numRuns; + m_numNotRuns = other.m_numNotRuns; + m_numPasses = other.m_numPasses; + m_numFailures = other.m_numFailures; + m_duration = other.m_duration; + } + + return *this; + } + + TestRun& TestRun::operator=(const TestRun& other) + { + if (this != &other) + { + TestSuiteContainer::operator=(other); + m_numRuns = other.m_numRuns; + m_numNotRuns = other.m_numNotRuns; + m_numPasses = other.m_numPasses; + m_numFailures = other.m_numFailures; + m_duration = other.m_duration; + } + + return *this; + } + + void TestRun::CalculateTestMetrics() + { + m_numRuns = 0; + m_numNotRuns = 0; + m_numPasses = 0; + m_numFailures = 0; + + for (const auto& suite : m_testSuites) + { + for (const auto& test : suite.m_tests) + { + if (test.m_status == TestRunStatus::Run) + { + m_numRuns++; + + if (test.m_result.value() == TestRunResult::Passed) + { + m_numPasses++; + } + else + { + m_numFailures++; + } + } + else + { + m_numNotRuns++; + } + } + } + } + + size_t TestRun::GetNumRuns() const + { + return m_numRuns; + } + + size_t TestRun::GetNumNotRuns() const + { + return m_numNotRuns; + } + + size_t TestRun::GetNumPasses() const + { + return m_numPasses; + } + + size_t TestRun::GetNumFailures() const + { + return m_numFailures; + } + + AZStd::chrono::milliseconds TestRun::GetDuration() const + { + return m_duration; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.h new file mode 100644 index 0000000000..f3a1ba93de --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRun.h @@ -0,0 +1,59 @@ +/* + * 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 + +namespace TestImpact +{ + //! Representation of a given test target's test run results. + class TestRun + : public TestSuiteContainer + { + using TestSuiteContainer = TestSuiteContainer; + + public: + TestRun(const TestRun&); + TestRun(TestRun&&) noexcept; + TestRun(const AZStd::vector& testSuites, AZStd::chrono::milliseconds duration); + TestRun(AZStd::vector&& testSuites, AZStd::chrono::milliseconds duration) noexcept; + + TestRun& operator=(const TestRun&); + TestRun& operator=(TestRun&&) noexcept; + + //! Returns the total number of tests that were run. + size_t GetNumRuns() const; + + //! Returns the total number of tests that were not run. + size_t GetNumNotRuns() const; + + //! Returns the total number of tests that were run and passed. + size_t GetNumPasses() const; + + //! Returns the total number of tests that were run and failed. + size_t GetNumFailures() const; + + //! Returns the duration of the job that was executed to yield this run data. + AZStd::chrono::milliseconds GetDuration() const; + + private: + void CalculateTestMetrics(); + + size_t m_numRuns = 0; + size_t m_numNotRuns = 0; + size_t m_numPasses = 0; + size_t m_numFailures = 0; + AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds{0}; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.cpp new file mode 100644 index 0000000000..a7ed446a25 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.cpp @@ -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. + * + */ + +#include + +namespace TestImpact +{ + TestRunJobData::TestRunJobData(const RepoPath& resultsArtifact) + : m_runArtifact(resultsArtifact) + { + } + + const RepoPath& TestRunJobData::GetRunArtifactPath() const + { + return m_runArtifact; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.h new file mode 100644 index 0000000000..a2033b5945 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunJobData.h @@ -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. + * + */ + +#pragma once + +#include + +namespace TestImpact +{ + //! Per-job data for test runs. + class TestRunJobData + { + public: + TestRunJobData(const RepoPath& resultsArtifact); + + //! Returns the path to the test run artifact produced by the test target. + const RepoPath& GetRunArtifactPath() const; + + private: + RepoPath m_runArtifact; //!< Path to results data. + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.cpp new file mode 100644 index 0000000000..15d3cc04b1 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.cpp @@ -0,0 +1,186 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace TestImpact +{ + namespace TestRunFields + { + // Keys for pertinent JSON node and attribute names + constexpr const char* Keys[] = + { + "suites", + "name", + "enabled", + "tests", + "duration", + "status", + "result" + }; + + enum + { + SuitesKey, + NameKey, + EnabledKey, + TestsKey, + DurationKey, + StatusKey, + ResultKey + }; + } // namespace + + AZStd::string SerializeTestRun(const TestRun& testRun) + { + rapidjson::StringBuffer stringBuffer; + rapidjson::PrettyWriter writer(stringBuffer); + + // Run + writer.StartObject(); + + // Run duration + writer.Key(TestRunFields::Keys[TestRunFields::DurationKey]); + writer.Uint(testRun.GetDuration().count()); + + // Suites + writer.Key(TestRunFields::Keys[TestRunFields::SuitesKey]); + writer.StartArray(); + + for (const auto& suite : testRun.GetTestSuites()) + { + // Suite + writer.StartObject(); + + // Suite name + writer.Key(TestRunFields::Keys[TestRunFields::NameKey]); + writer.String(suite.m_name.c_str()); + + // Suite duration + writer.Key(TestRunFields::Keys[TestRunFields::DurationKey]); + writer.Uint(suite.m_duration.count()); + + // Suite enabled + writer.Key(TestRunFields::Keys[TestRunFields::EnabledKey]); + writer.Bool(suite.m_enabled); + + // Suite tests + writer.Key(TestRunFields::Keys[TestRunFields::TestsKey]); + writer.StartArray(); + for (const auto& test : suite.m_tests) + { + // Test + writer.StartObject(); + + // Test name + writer.Key(TestRunFields::Keys[TestRunFields::NameKey]); + writer.String(test.m_name.c_str()); + + // Test enabled + writer.Key(TestRunFields::Keys[TestRunFields::EnabledKey]); + writer.Bool(test.m_enabled); + + // Test duration + writer.Key(TestRunFields::Keys[TestRunFields::DurationKey]); + writer.Uint(test.m_duration.count()); + + // Test status + writer.Key(TestRunFields::Keys[TestRunFields::StatusKey]); + writer.Bool(static_cast(test.m_status)); + + // Test result + if (test.m_status == TestRunStatus::Run) + { + writer.Key(TestRunFields::Keys[TestRunFields::ResultKey]); + writer.Bool(static_cast(test.m_result.value())); + } + else + { + writer.Key(TestRunFields::Keys[TestRunFields::ResultKey]); + writer.Null(); + } + + // End test + writer.EndObject(); + } + + // End tests + writer.EndArray(); + + // End suite + writer.EndObject(); + } + + // End suites + writer.EndArray(); + + // End run + writer.EndObject(); + + return stringBuffer.GetString(); + } + + TestRun DeserializeTestRun(const AZStd::string& testEnumString) + { + AZStd::vector testSuites; + rapidjson::Document doc; + + if (doc.Parse<0>(testEnumString.c_str()).HasParseError()) + { + throw TestEngineException("Could not parse enumeration data"); + } + + // Run duration + const AZStd::chrono::milliseconds runDuration = AZStd::chrono::milliseconds{doc[TestRunFields::Keys[TestRunFields::DurationKey]].GetUint()}; + + // Suites + for (const auto& suite : doc[TestRunFields::Keys[TestRunFields::SuitesKey]].GetArray()) + { + // Suite name + const AZStd::string name = suite[TestRunFields::Keys[TestRunFields::NameKey]].GetString(); + + // Suite duration + const AZStd::chrono::milliseconds suiteDuration = AZStd::chrono::milliseconds{suite[TestRunFields::Keys[TestRunFields::DurationKey]].GetUint()}; + + // Suite enabled + const bool enabled = suite[TestRunFields::Keys[TestRunFields::EnabledKey]].GetBool(); + + testSuites.emplace_back(TestRunSuite{ + suite[TestRunFields::Keys[TestRunFields::NameKey]].GetString(), + suite[TestRunFields::Keys[TestRunFields::EnabledKey]].GetBool(), + {}, + AZStd::chrono::milliseconds{suite[TestRunFields::Keys[TestRunFields::DurationKey]].GetUint()}}); + + // Suite tests + for (const auto& test : suite[TestRunFields::Keys[TestRunFields::TestsKey]].GetArray()) + { + AZStd::optional result; + TestRunStatus status = static_cast(test[TestRunFields::Keys[TestRunFields::StatusKey]].GetBool()); + if (status == TestRunStatus::Run) + { + result = static_cast(test[TestRunFields::Keys[TestRunFields::ResultKey]].GetBool()); + } + const AZStd::chrono::milliseconds testDuration = AZStd::chrono::milliseconds{test[TestRunFields::Keys[TestRunFields::DurationKey]].GetUint()}; + testSuites.back().m_tests.emplace_back( + TestRunCase{test[TestRunFields::Keys[TestRunFields::NameKey]].GetString(), test[TestRunFields::Keys[TestRunFields::EnabledKey]].GetBool(), result, testDuration, status}); + } + } + + return TestRun(std::move(testSuites), runDuration); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.h new file mode 100644 index 0000000000..a73e675559 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunSerializer.h @@ -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 + +#include + +#include + +namespace TestImpact +{ + //! Serializes the specified test run to JSON format. + AZStd::string SerializeTestRun(const TestRun& testRun); + + //! Deserializes a test run from the specified test run data in JSON format. + TestRun DeserializeTestRun(const AZStd::string& testRunString); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.cpp new file mode 100644 index 0000000000..16717a1fe5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.cpp @@ -0,0 +1,68 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + TestRunner::TestRunner(size_t maxConcurrentRuns) + : JobRunner(maxConcurrentRuns) + { + } + + AZStd::pair> TestRunner::RunTests( + const AZStd::vector& jobInfos, + AZStd::optional runTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback) + { + const auto payloadGenerator = [this](const JobDataMap& jobDataMap) + { + PayloadMap runs; + for (const auto& [jobId, jobData] : jobDataMap) + { + const auto& [meta, jobInfo] = jobData; + if (meta.m_result == JobResult::ExecutedWithSuccess || meta.m_result == JobResult::ExecutedWithFailure) + { + try + { + runs[jobId] = TestRun(GTest::TestRunSuitesFactory(ReadFileContents(jobInfo->GetRunArtifactPath())), meta.m_duration.value()); + } + catch (const Exception& e) + { + AZ_Printf("RunTests", AZStd::string::format("%s\n", e.what()).c_str()); + runs[jobId] = AZStd::nullopt; + } + } + } + + return runs; + }; + + return ExecuteJobs( + jobInfos, + payloadGenerator, + StdOutputRouting::None, + StdErrorRouting::None, + runTimeout, + runnerTimeout, + clientCallback, + AZStd::nullopt); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.h new file mode 100644 index 0000000000..eaa7b6de1d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/Run/TestImpactTestRunner.h @@ -0,0 +1,46 @@ +/* + * 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 TestImpact +{ + //! Runs a batch of test targets to determine the test passes/failures. + class TestRunner + : public TestJobRunner + { + using JobRunner = TestJobRunner; + + public: + //! Constructs a test runner with the specified parameters common to all job runs of this runner. + //! @param maxConcurrentRuns The maximum number of runs to be in flight at any given time. + explicit TestRunner(size_t maxConcurrentRuns); + + //! Executes the specified test run jobs according to the specified job exception policies. + //! @param jobInfos The test run jobs to execute. + //! @param jobExceptionPolicy The test run job exception policy to be used for this run (use + //! TestJobExceptionPolicy::OnFailedToExecute to throw on test failures). + //! @param runTimeout The maximum duration a run may be in-flight for before being forcefully terminated. + //! @param runnerTimeout The maximum duration the runner may run before forcefully terminating all in-flight runs. + //! @param clientCallback The optional client callback to be called whenever a run job changes state. + //! @return The result of the run sequence and the run jobs with their associated test run payloads. + AZStd::pair> RunTests( + const AZStd::vector& jobInfos, + AZStd::optional runTimeout, + AZStd::optional runnerTimeout, + AZStd::optional clientCallback); + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.cpp new file mode 100644 index 0000000000..a9d0e15781 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.cpp @@ -0,0 +1,351 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + namespace + { + // Calculate the sequence result by analysing the state of the test targets that were run. + template + TestSequenceResult CalculateSequenceResult( + ProcessSchedulerResult result, + const AZStd::vector& engineJobs, + Policy::ExecutionFailure executionFailurePolicy) + { + if (result == ProcessSchedulerResult::Timeout) + { + // Test job runner timing out overrules all other possible sequence results + return TestSequenceResult::Timeout; + } + + bool hasExecutionFailures = false; + bool hasTestFailures = false; + for (const auto& engineJob : engineJobs) + { + switch (engineJob.GetTestResult()) + { + case Client::TestRunResult::FailedToExecute: + { + hasExecutionFailures = true; + break; + } + case Client::TestRunResult::Timeout: + case Client::TestRunResult::TestFailures: + { + hasTestFailures = true; + break; + } + default: + { + continue; + } + } + } + + // Execution failure can be considered test passes if a permissive execution failure policy is used, otherwise they are failures + if ((hasExecutionFailures && executionFailurePolicy != Policy::ExecutionFailure::Ignore) || hasTestFailures) + { + return TestSequenceResult::Failure; + } + else + { + return TestSequenceResult::Success; + } + } + + // Deduces the run result for a given test target based on how the process exited and known return values + Client::TestRunResult GetClientTestRunResultForMeta(const JobMeta& meta) + { + // Attempt to determine why a given test target executed successfully but return with an error code + if (meta.m_returnCode.has_value()) + { + if (const auto result = CheckForAnyKnownErrorCode(meta.m_returnCode.value()); + result != AZStd::nullopt) + { + return result.value(); + } + } + + switch (meta.m_result) + { + // If the test target executed successfully but returned in an unknown abnormal state it's probably because a test caused + // an unhandled exception, segfault or any other of the weird and wonderful ways a badly behaving test can terminate + case JobResult::ExecutedWithFailure: + return Client::TestRunResult::TestFailures; + // The trivial case: all of the tests in the test target passed + case JobResult::ExecutedWithSuccess: + return Client::TestRunResult::AllTestsPass; + // NotExecuted happens when a test is queued for launch but the test runner terminates the sequence (either due to client abort + // or due to the sequence timer expiring) whereas Terminated happens when the aforementioned scenarios happen when the test target + // is in flight + case JobResult::NotExecuted: + case JobResult::Terminated: + return Client::TestRunResult::NotRun; + // The individual timer for the test target expired + case JobResult::Timeout: + return Client::TestRunResult::Timeout; + default: + throw(TestEngineException(AZStd::string::format("Unexpected job result: %u", static_cast(meta.m_result)))); + } + } + + // Map for storing the test engine job data of completed test target runs + template + using TestEngineJobMap = AZStd::unordered_map; + + // Helper trait for identifying the test engine job specialization for a given test job runner + template + struct TestJobRunnerTrait + {}; + + // Helper function for getting the type directly of the test job runner trait + template + using TestEngineJobType = typename TestJobRunnerTrait::TestEngineJobType; + + // Type trait for the test enumerator + template<> + struct TestJobRunnerTrait + { + using TestEngineJobType = TestEngineEnumeration; + }; + + // Type trait for the test runner + template<> + struct TestJobRunnerTrait + { + using TestEngineJobType = TestEngineRegularRun; + }; + + // Type trait for the instrumented test runner + template<> + struct TestJobRunnerTrait + { + using TestEngineJobType = TestEngineInstrumentedRun; + }; + + // Functor for handling test job runner callbacks + template + class TestJobRunnerCallbackHandler + { + using IdType = typename TestJobRunner::JobInfo::IdType; + using JobInfo = typename TestJobRunner::JobInfo; + public: + TestJobRunnerCallbackHandler( + const AZStd::vector& testTargets, + TestEngineJobMap* engineJobs, + Policy::ExecutionFailure executionFailurePolicy, + Policy::TestFailure testFailurePolicy, + AZStd::optional* callback) + : m_testTargets(testTargets) + , m_engineJobs(engineJobs) + , m_executionFailurePolicy(executionFailurePolicy) + , m_testFailurePolicy(testFailurePolicy) + , m_callback(callback) + { + } + + [[nodiscard]] ProcessCallbackResult operator()(const typename JobInfo& jobInfo, const TestImpact::JobMeta& meta) + { + const auto id = jobInfo.GetId().m_value; + const auto& args = jobInfo.GetCommand().m_args; + const auto* target = m_testTargets[id]; + const auto result = GetClientTestRunResultForMeta(meta); + + // Place the test engine job associated with this test run into the map along with its client test run result so + // that it can be retrieved when the sequence has ended (and any associated artifacts processed) + const auto& [it, success] = m_engineJobs->emplace(id, TestEngineJob(target, args, meta, result)); + + if (m_callback->has_value()) + { + (*m_callback).value()(it->second); + } + + if ((result == Client::TestRunResult::FailedToExecute && m_executionFailurePolicy == Policy::ExecutionFailure::Abort) || + (result == Client::TestRunResult::TestFailures && m_testFailurePolicy == Policy::TestFailure::Abort)) + { + return ProcessCallbackResult::Abort; + } + + return ProcessCallbackResult::Continue; + } + + private: + const AZStd::vector& m_testTargets; + TestEngineJobMap* m_engineJobs; + Policy::ExecutionFailure m_executionFailurePolicy; + Policy::TestFailure m_testFailurePolicy; + AZStd::optional* m_callback; + }; + + // Helper function to compile the run type specific test engine jobs from their associated jobs and payloads + template + AZStd::vector> CompileTestEngineRuns( + const AZStd::vector& testTargets, + AZStd::vector& runnerjobs, + TestEngineJobMap&& engineJobs) + { + AZStd::vector> engineRuns; + engineRuns.reserve(testTargets.size()); + + for (auto& job : runnerjobs) + { + const auto id = job.GetJobInfo().GetId().m_value; + if (auto it = engineJobs.find(id); + it != engineJobs.end()) + { + // An entry in the test engine job map means that this job was acted upon (an attempt to execute, successful or otherwise) + auto& engineJob = it->second; + TestEngineJobType run(AZStd::move(engineJob), job.ReleasePayload()); + engineRuns.push_back(AZStd::move(run)); + } + else + { + // No entry in the test engine job map means that this job never had the opportunity to be acted upon (the sequence + // was terminated whilst this job was still queued up for execution) + const auto& args = job.GetJobInfo().GetCommand().m_args; + const auto* target = testTargets[id]; + TestEngineJobType run(TestEngineJob(target, args, {}, Client::TestRunResult::NotRun), {}); + engineRuns.push_back(AZStd::move(run)); + } + } + + return engineRuns; + } + } + + TestEngine::TestEngine( + const RepoPath& sourceDir, + const RepoPath& targetBinaryDir, + const RepoPath& cacheDir, + const RepoPath& artifactDir, + const RepoPath& testRunnerBinary, + const RepoPath& instrumentBinary, + size_t maxConcurrentRuns) + : m_maxConcurrentRuns(maxConcurrentRuns) + , m_testJobInfoGenerator(AZStd::make_unique( + sourceDir, targetBinaryDir, cacheDir, artifactDir, testRunnerBinary, instrumentBinary)) + , m_testEnumerator(AZStd::make_unique(maxConcurrentRuns)) + , m_instrumentedTestRunner(AZStd::make_unique(maxConcurrentRuns)) + , m_testRunner(AZStd::make_unique(maxConcurrentRuns)) + , m_artifactDir(artifactDir) + { + } + + TestEngine::~TestEngine() = default; + + void TestEngine::DeleteArtifactXmls() const + { + DeleteFiles(m_artifactDir, "*.xml"); + } + + AZStd::pair> TestEngine::UpdateEnumerationCache( + const AZStd::vector& testTargets, + Policy::ExecutionFailure executionFailurePolicy, + Policy::TestFailure testFailurePolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback) + { + TestEngineJobMap engineJobs; + const auto jobInfos = m_testJobInfoGenerator->GenerateTestEnumerationJobInfos(testTargets, TestEnumerator::JobInfo::CachePolicy::Write); + + auto [result, runnerJobs] = m_testEnumerator->Enumerate( + jobInfos, + testTargetTimeout, + globalTimeout, + TestJobRunnerCallbackHandler(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback)); + + auto engineRuns = CompileTestEngineRuns(testTargets, runnerJobs, AZStd::move(engineJobs)); + return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) }; + } + + AZStd::pair> TestEngine::RegularRun( + const AZStd::vector& testTargets, + [[maybe_unused]]Policy::TestSharding testShardingPolicy, + Policy::ExecutionFailure executionFailurePolicy, + Policy::TestFailure testFailurePolicy, + [[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback) + { + DeleteArtifactXmls(); + + TestEngineJobMap engineJobs; + const auto jobInfos = m_testJobInfoGenerator->GenerateRegularTestRunJobInfos(testTargets); + + TestJobRunnerCallbackHandler jobCallback(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback); + auto [result, runnerJobs] = m_testRunner->RunTests( + jobInfos, + testTargetTimeout, + globalTimeout, + jobCallback); + + auto engineRuns = CompileTestEngineRuns(testTargets, runnerJobs, AZStd::move(engineJobs)); + return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) }; + } + + AZStd::pair> TestEngine::InstrumentedRun( + const AZStd::vector& testTargets, + [[maybe_unused]] Policy::TestSharding testShardingPolicy, + Policy::ExecutionFailure executionFailurePolicy, + Policy::IntegrityFailure integrityFailurePolicy, + Policy::TestFailure testFailurePolicy, + [[maybe_unused]]Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback) + { + DeleteArtifactXmls(); + + TestEngineJobMap engineJobs; + const auto jobInfos = m_testJobInfoGenerator->GenerateInstrumentedTestRunJobInfos(testTargets, CoverageLevel::Source); + + auto [result, runnerJobs] = m_instrumentedTestRunner->RunInstrumentedTests( + jobInfos, + testTargetTimeout, + globalTimeout, + TestJobRunnerCallbackHandler(testTargets, &engineJobs, executionFailurePolicy, testFailurePolicy, &callback)); + + auto engineRuns = CompileTestEngineRuns(testTargets, runnerJobs, AZStd::move(engineJobs)); + + // Now that we know the true result of successful jobs that return non-zero we can deduce if we have any integrity failures + // where a test target ran and completed its tests without incident yet failed to produce coverage data + if (integrityFailurePolicy == Policy::IntegrityFailure::Abort) + { + for (const auto& engineRun : engineRuns) + { + if (const auto testResult = engineRun.GetTestResult(); + testResult == Client::TestRunResult::AllTestsPass || testResult == Client::TestRunResult::TestFailures) + { + AZ_TestImpact_Eval(engineRun.GetTestCoverge().has_value(), TestEngineException, AZStd::string::format( + "Test target %s completed its test run but failed to produce coverage data", engineRun.GetTestTarget()->GetName().c_str())); + } + } + } + + return { CalculateSequenceResult(result, engineRuns, executionFailurePolicy), AZStd::move(engineRuns) }; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.h new file mode 100644 index 0000000000..7d16f352f3 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngine.h @@ -0,0 +1,131 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace TestImpact +{ + class TestTarget; + class TestJobInfoGenerator; + class TestEnumerator; + class InstrumentedTestRunner; + class TestRunner; + + //! Callback for when a given test engine job completes. + using TestEngineJobCompleteCallback = AZStd::function; + + //! Provides the front end for performing test enumerations and test runs. + class TestEngine + { + public: + //! Configures the test engine with the necessary path information for launching test targets and managing the artifacts they produce. + //! @param sourceDir Root path where source files are found (including subfolders). + //! @param targetBinaryDir Path to where the test target binaries are found. + //! @param cacheDir Path to the persistent folder where test target enumerations are cached. + //! @param artifactDir Path to the transient directory where test artifacts are produced. + //! @param testRunnerBinary Path to the binary responsible for launching test targets that have the TestRunner launch method. + //! @param instrumentBinary Path to the binary responsible for launching test targets with test coverage instrumentation. + //! @param maxConcurrentRuns The maximum number of concurrent test targets that can be in flight at any given moment. + TestEngine( + const RepoPath& sourceDir, + const RepoPath& targetBinaryDir, + const RepoPath& cacheDir, + const RepoPath& artifactDir, + const RepoPath& testRunnerBinary, + const RepoPath& instrumentBinary, + size_t maxConcurrentRuns); + + ~TestEngine(); + + //! Updates the cached enumerations for the specified test targets. + //! @note Whilst test runs will make use of this cache for test target sharding it is the responsibility of the client to + //! ensure any stale caches are up to date by calling this function. No attempt to maintain internal consistency will be made + //! by the test engine itself. + //! @param testTargets The test targets to enumerate. + //! @param executionFailurePolicy The policy for how enumeration execution failures should be handled. + //! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty). + //! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty). + //! @param callback The client callback function to handle completed test target enumerations. + //! @ returns The sequence result and the enumerations for the target that were enumerated. + AZStd::pair> UpdateEnumerationCache( + const AZStd::vector& testTargets, + Policy::ExecutionFailure executionFailurePolicy, + Policy::TestFailure testFailurePolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback); + + //! Performs a test run without any instrumentation and, for each test target, returns the test run results and metrics about the run. + //! @param testTargets The test targets to run. + //! @param testShardingPolicy Test sharding policy to use for test targets in this run. + //! @param executionFailurePolicy Policy for how test execution failures should be handled. + //! @param testFailurePolicy Policy for how test targets with failing tests should be handled. + //! @param targetOutputCapture Policy for how test target standard output should be captured and handled. + //! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty). + //! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty). + //! @param callback The client callback function to handle completed test target runs. + //! @ returns The sequence result and the test run results for the test targets that were run. + [[nodiscard]] AZStd::pair> RegularRun( + const AZStd::vector& testTargets, + Policy::TestSharding testShardingPolicy, + Policy::ExecutionFailure executionFailurePolicy, + Policy::TestFailure testFailurePolicy, + Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback); + + //! Performs a test run with instrumentation and, for each test target, returns the test run results, coverage data and metrics about the run. + //! @param testTargets The test targets to run. + //! @param testShardingPolicy Test sharding policy to use for test targets in this run. + //! @param executionFailurePolicy Policy for how test execution failures should be handled. + //! @param integrityFailurePolicy Policy for how integrty failures of the test impact data and source tree model should be handled. + //! @param testFailurePolicy Policy for how test targets with failing tests should be handled. + //! @param targetOutputCapture Policy for how test target standard output should be captured and handled. + //! @param testTargetTimeout The maximum duration a test target may be in-flight for before being forcefully terminated (infinite if empty). + //! @param globalTimeout The maximum duration the enumeration sequence may run before being forcefully terminated (infinite if empty). + //! @param callback The client callback function to handle completed test target runs. + //! @ returns The sequence result and the test run results and test coverages for the test targets that were run. + [[nodiscard]] AZStd::pair> InstrumentedRun( + const AZStd::vector& testTargets, + Policy::TestSharding testShardingPolicy, + Policy::ExecutionFailure executionFailurePolicy, + Policy::IntegrityFailure integrityFailurePolicy, + Policy::TestFailure testFailurePolicy, + Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional callback); + + private: + //! Cleans up the artifacts directory of any artifacts from previous runs. + void DeleteArtifactXmls() const; + + size_t m_maxConcurrentRuns = 0; + AZStd::unique_ptr m_testJobInfoGenerator; + AZStd::unique_ptr m_testEnumerator; + AZStd::unique_ptr m_instrumentedTestRunner; + AZStd::unique_ptr m_testRunner; + RepoPath m_artifactDir; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.cpp new file mode 100644 index 0000000000..3c114d1b33 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.cpp @@ -0,0 +1,27 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + TestEngineEnumeration::TestEngineEnumeration(TestEngineJob&& job, AZStd::optional&& enumeration) + : TestEngineJob(AZStd::move(job)) + , m_enumeration(AZStd::move(enumeration)) + { + } + + const AZStd::optional& TestEngineEnumeration::GetTestEnumeration() const + { + return m_enumeration; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.h new file mode 100644 index 0000000000..28473a08bc --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineEnumeration.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 + +namespace TestImpact +{ + //! Represents the generated test enumeration data for a test engine enumeration. + class TestEngineEnumeration + : public TestEngineJob + { + public: + TestEngineEnumeration(TestEngineJob&& job, AZStd::optional&& enumeration); + + //! Returns the test enumeration payload for this job (if any). + const AZStd::optional& GetTestEnumeration() const; + private: + AZStd::optional m_enumeration; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineException.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineException.h new file mode 100644 index 0000000000..cf10b68b24 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineException.h @@ -0,0 +1,25 @@ +/* + * 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 TestImpact +{ + //! Exception for test engine runs and related operations. + class TestEngineException : public Exception + { + public: + using Exception::Exception; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.cpp new file mode 100644 index 0000000000..20c80c5f5f --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.cpp @@ -0,0 +1,52 @@ +/* + * 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 TestImpact +{ + namespace + { + AZStd::optional ReleaseTestRun(AZStd::optional>& testRunAndCoverage) + { + if (testRunAndCoverage.has_value()) + { + return AZStd::move(testRunAndCoverage.value().first); + } + + return AZStd::nullopt; + } + + AZStd::optional ReleaseTestCoverage(AZStd::optional>& testRunAndCoverage) + { + if (testRunAndCoverage.has_value()) + { + return AZStd::move(testRunAndCoverage.value().second); + } + + return AZStd::nullopt; + } + } + + TestEngineInstrumentedRun::TestEngineInstrumentedRun(TestEngineJob&& testJob, AZStd::optional>&& testRunAndCoverage) + : TestEngineRegularRun(AZStd::move(testJob), ReleaseTestRun(testRunAndCoverage)) + , m_testCoverage(ReleaseTestCoverage(testRunAndCoverage)) + { + } + + const AZStd::optional& TestEngineInstrumentedRun::GetTestCoverge() const + { + return m_testCoverage; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.h new file mode 100644 index 0000000000..efecebcbd4 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineInstrumentedRun.h @@ -0,0 +1,33 @@ +/* + * 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 + +namespace TestImpact +{ + //! Represents the generated test run and coverage data for an instrumented regular test engine run. + class TestEngineInstrumentedRun + : public TestEngineRegularRun + { + public: + TestEngineInstrumentedRun(TestEngineJob&& testJob, AZStd::optional>&& testRunAndCoverage); + + //! Returns the test coverage payload for this job (if any). + const AZStd::optional& GetTestCoverge() const; + + private: + AZStd::optional m_testCoverage; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.cpp new file mode 100644 index 0000000000..26aa71adcd --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.cpp @@ -0,0 +1,40 @@ +/* + * 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 + +namespace TestImpact +{ + TestEngineJob::TestEngineJob(const TestTarget* testTarget, const AZStd::string& commandString, const JobMeta& jobMeta, Client::TestRunResult testResult) + : JobMetaWrapper(jobMeta) + , m_testTarget(testTarget) + , m_commandString(commandString) + , m_testResult(testResult) + { + } + + const TestTarget* TestEngineJob::GetTestTarget() const + { + return m_testTarget; + } + + const AZStd::string& TestEngineJob::GetCommandString() const + { + return m_commandString; + } + + Client::TestRunResult TestEngineJob::GetTestResult() const + { + return m_testResult; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.h new file mode 100644 index 0000000000..753a4cd494 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJob.h @@ -0,0 +1,42 @@ +/* + * 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 TestImpact +{ + class TestTarget; + + //! Represents the meta-data describing a test engine run. + class TestEngineJob + : public JobMetaWrapper + { + public: + TestEngineJob(const TestTarget* testTarget, const AZStd::string& commandString, const JobMeta& jobMeta, Client::TestRunResult testResult); + + //! Returns the test target that was run for this job. + const TestTarget* GetTestTarget() const; + + //! Returns the result of the job that was run. + Client::TestRunResult GetTestResult() const; + + //! Returns the command string that was used to execute this job. + const AZStd::string& GetCommandString() const; + + private: + const TestTarget* m_testTarget; + AZStd::string m_commandString; + Client::TestRunResult m_testResult; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.cpp new file mode 100644 index 0000000000..931e3a272b --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.cpp @@ -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. + * + */ + +#include + +namespace TestImpact +{ + // Known error codes for test runner and test library + namespace ErrorCodes + { + namespace GTest + { + static constexpr ReturnCode Unsuccessful = 1; + } + + namespace AZTestRunner + { + static constexpr ReturnCode InvalidArgs = 101; + static constexpr ReturnCode FailedToFindTargetBinary = 102; + static constexpr ReturnCode SymbolNotFound = 103; + static constexpr ReturnCode ModuleSkipped = 104; + } + } + + AZStd::optional CheckForKnownTestRunnerErrorCode(int returnCode) + { + switch (returnCode) + { + // We will consider test targets that technically execute but their launcher or unit test library return a know error + // code that pertains to incorrect argument usage as test targets that failed to execute + case ErrorCodes::AZTestRunner::InvalidArgs: + case ErrorCodes::AZTestRunner::FailedToFindTargetBinary: + case ErrorCodes::AZTestRunner::ModuleSkipped: + case ErrorCodes::AZTestRunner::SymbolNotFound: + return Client::TestRunResult::FailedToExecute; + default: + return AZStd::nullopt; + } + } + + AZStd::optional CheckForKnownTestLibraryErrorCode(int returnCode) + { + if (returnCode == ErrorCodes::GTest::Unsuccessful) + { + return Client::TestRunResult::TestFailures; + } + + return AZStd::nullopt; + } + + AZStd::optional CheckForAnyKnownErrorCode(ReturnCode returnCode) + { + if (const auto result = CheckForKnownTestInstrumentErrorCode(returnCode); + result != AZStd::nullopt) + { + return result.value(); + } + + if (const auto result = CheckForKnownTestRunnerErrorCode(returnCode); + result != AZStd::nullopt) + { + return result.value(); + } + + if (const auto result = CheckForKnownTestLibraryErrorCode(returnCode); + result != AZStd::nullopt) + { + return result.value(); + } + + return AZStd::nullopt; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.h new file mode 100644 index 0000000000..969aad9c84 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineJobFailure.h @@ -0,0 +1,34 @@ +/* + * 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 TestImpact +{ + //! Checks for known test instrumentation error return codes and returns the corresponding client test run result or empty. + AZStd::optional CheckForKnownTestInstrumentErrorCode(ReturnCode returnCode); + + //! Checks for known test runner error return codes and returns the corresponding client test run result or empty. + AZStd::optional CheckForKnownTestRunnerErrorCode(ReturnCode returnCode); + + //! Checks for known test library error return codes and returns the corresponding client test run result or empty. + AZStd::optional CheckForKnownTestLibraryErrorCode(ReturnCode returnCode); + + //! Checks for all known error return codes and returns the corresponding client test run result or empty. + AZStd::optional CheckForAnyKnownErrorCode(ReturnCode returnCode); +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.cpp new file mode 100644 index 0000000000..0090689e8b --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.cpp @@ -0,0 +1,27 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + TestEngineRegularRun::TestEngineRegularRun(TestEngineJob&& testJob, AZStd::optional&& testRun) + : TestEngineJob(AZStd::move(testJob)) + , m_testRun(AZStd::move(testRun)) + { + } + + const AZStd::optional& TestEngineRegularRun::GetTestRun() const + { + return m_testRun; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.h new file mode 100644 index 0000000000..3c65830094 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestEngineRegularRun.h @@ -0,0 +1,34 @@ +/* + * 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 TestImpact +{ + //! Represents the generated test run data for a regular test engine run. + class TestEngineRegularRun + : public TestEngineJob + { + public: + TestEngineRegularRun(TestEngineJob&& testJob, AZStd::optional&& testRun); + + //! Returns the test run payload for this job (if any). + const AZStd::optional& GetTestRun() const; + private: + AZStd::optional m_testRun; + }; +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestSuiteContainer.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestSuiteContainer.h new file mode 100644 index 0000000000..5991640c46 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestEngine/TestImpactTestSuiteContainer.h @@ -0,0 +1,168 @@ +/* + * 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 TestImpact +{ + //! Encapsulation of test suites into a class with meta-data about each the suites. + //! @tparam TestSuite The test suite data structure to encapsulate. + template + class TestSuiteContainer + { + public: + TestSuiteContainer(const TestSuiteContainer&); + TestSuiteContainer(TestSuiteContainer&&) noexcept; + TestSuiteContainer(const AZStd::vector& testSuites); + TestSuiteContainer(AZStd::vector&& testSuites) noexcept; + + TestSuiteContainer& operator=(const TestSuiteContainer&); + TestSuiteContainer& operator=(TestSuiteContainer&&) noexcept; + + //! Returns the test suites in this container. + const AZStd::vector& GetTestSuites() const; + + //! Returns the number of test suites in this container. + size_t GetNumTestSuites() const; + + //! Returns the total number of tests across all test suites. + size_t GetNumTests() const; + + //! Returns the total number of enabled tests across all test suites. + size_t GetNumEnabledTests() const; + + //! Returns the total number of disabled tests across all test suites. + size_t GetNumDisabledTests() const; + + private: + void CalculateTestMetrics(); + + protected: + AZStd::vector m_testSuites; + size_t m_numDisabledTests = 0; + size_t m_numEnabledTests = 0; + }; + + template + TestSuiteContainer::TestSuiteContainer(TestSuiteContainer&& other) noexcept + : m_testSuites(AZStd::move(other.m_testSuites)) + , m_numDisabledTests(other.m_numDisabledTests) + , m_numEnabledTests(other.m_numEnabledTests) + { + } + + template + TestSuiteContainer::TestSuiteContainer(const TestSuiteContainer& other) + : m_testSuites(other.m_testSuites.begin(), other.m_testSuites.end()) + , m_numDisabledTests(other.m_numDisabledTests) + , m_numEnabledTests(other.m_numEnabledTests) + { + } + + template + TestSuiteContainer::TestSuiteContainer(AZStd::vector&& testSuites) noexcept + : m_testSuites(std::move(testSuites)) + { + CalculateTestMetrics(); + } + + template + TestSuiteContainer::TestSuiteContainer(const AZStd::vector& testSuites) + : m_testSuites(testSuites) + { + CalculateTestMetrics(); + } + + template + TestSuiteContainer& TestSuiteContainer::operator=(TestSuiteContainer&& other) noexcept + { + if (this != &other) + { + m_testSuites = AZStd::move(other.m_testSuites); + m_numDisabledTests = other.m_numDisabledTests; + m_numEnabledTests = other.m_numEnabledTests; + } + + return *this; + } + + template + TestSuiteContainer& TestSuiteContainer::operator=(const TestSuiteContainer& other) + { + if (this != &other) + { + m_testSuites = other.m_testSuites; + m_numDisabledTests = other.m_numDisabledTests; + m_numEnabledTests = other.m_numEnabledTests; + } + + return *this; + } + + template + void TestSuiteContainer::CalculateTestMetrics() + { + m_numDisabledTests = 0; + m_numEnabledTests = 0; + + for (const auto& suite : m_testSuites) + { + if (suite.m_enabled) + { + const auto enabled = std::count_if(suite.m_tests.begin(), suite.m_tests.end(), [](const auto& test) + { + return test.m_enabled; + }); + + m_numEnabledTests += enabled; + m_numDisabledTests += suite.m_tests.size() - enabled; + } + else + { + // Disabled status of suites propagates down to all tests regardless of whether or not each individual test is disabled + m_numDisabledTests += suite.m_tests.size(); + } + } + } + + template + const AZStd::vector& TestSuiteContainer::GetTestSuites() const + { + return m_testSuites; + } + + template + size_t TestSuiteContainer::GetNumTests() const + { + return m_numEnabledTests + m_numDisabledTests; + } + + template + size_t TestSuiteContainer::GetNumEnabledTests() const + { + return m_numEnabledTests; + } + + template + size_t TestSuiteContainer::GetNumDisabledTests() const + { + return m_numDisabledTests; + } + + template + size_t TestSuiteContainer::GetNumTestSuites() const + { + return m_testSuites.size(); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactChangeListSerializer.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactChangeListSerializer.cpp new file mode 100644 index 0000000000..ef157f1848 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactChangeListSerializer.cpp @@ -0,0 +1,96 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace TestImpact +{ + namespace ChangeListFields + { + // Keys for pertinent JSON node and attribute names + constexpr const char* Keys[] = + { + "createdFiles", + "updatedFiles", + "deletedFiles" + }; + + enum + { + CreateKey, + UpdateKey, + DeleteKey + }; + } // namespace + + AZStd::string SerializeChangeList(const ChangeList& changeList) + { + rapidjson::StringBuffer stringBuffer; + rapidjson::PrettyWriter writer(stringBuffer); + + const auto serializeFileList = [&writer](const char* key, const AZStd::vector& fileList) + { + writer.Key(key); + writer.StartArray(); + + for (const auto& file : fileList) + { + writer.String(file.c_str()); + } + + writer.EndArray(); + }; + + writer.StartObject(); + serializeFileList(ChangeListFields::Keys[ChangeListFields::CreateKey], changeList.m_createdFiles); + serializeFileList(ChangeListFields::Keys[ChangeListFields::UpdateKey], changeList.m_updatedFiles); + serializeFileList(ChangeListFields::Keys[ChangeListFields::DeleteKey], changeList.m_deletedFiles); + writer.EndObject(); + + return stringBuffer.GetString(); + } + + ChangeList DeserializeChangeList(const AZStd::string& changeListString) + { + ChangeList changeList; + rapidjson::Document doc; + + if (doc.Parse<0>(changeListString.c_str()).HasParseError()) + { + throw ChangeListException("Could not parse change list data"); + } + + const auto deserializeFileList = [&doc](const char* key) + { + AZStd::vector fileList; + + for (const auto& file : doc[key].GetArray()) + { + fileList.push_back(file.GetString()); + } + + return fileList; + }; + + changeList.m_createdFiles = deserializeFileList(ChangeListFields::Keys[ChangeListFields::CreateKey]); + changeList.m_updatedFiles = deserializeFileList(ChangeListFields::Keys[ChangeListFields::UpdateKey]); + changeList.m_deletedFiles = deserializeFileList(ChangeListFields::Keys[ChangeListFields::DeleteKey]); + + return changeList; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientFailureReport.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientFailureReport.cpp new file mode 100644 index 0000000000..d8e4709f21 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientFailureReport.cpp @@ -0,0 +1,124 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + namespace Client + { + TargetFailure::TargetFailure(const AZStd::string& targetName) + : m_targetName(targetName) + { + } + + const AZStd::string& TargetFailure::GetTargetName() const + { + return m_targetName; + } + + ExecutionFailure::ExecutionFailure(const AZStd::string& targetName, const AZStd::string& command) + : TargetFailure(targetName) + , m_commandString(command) + { + } + + const AZStd::string& ExecutionFailure::GetCommandString() const + { + return m_commandString; + } + + TestFailure::TestFailure(const AZStd::string& testName, const AZStd::string& errorMessage) + : m_name(testName) + , m_errorMessage(errorMessage) + { + } + + const AZStd::string& TestFailure::GetName() const + { + return m_name; + } + + const AZStd::string& TestFailure::GetErrorMessage() const + { + return m_errorMessage; + } + + TestCaseFailure::TestCaseFailure(const AZStd::string& testCaseName, AZStd::vector&& testFailures) + : m_name(testCaseName) + , m_testFailures(AZStd::move(testFailures)) + { + } + + const AZStd::string& TestCaseFailure::GetName() const + { + return m_name; + } + + const AZStd::vector& TestCaseFailure::GetTestFailures() const + { + return m_testFailures; + } + + TestRunFailure::TestRunFailure(const AZStd::string& targetName, AZStd::vector&& testFailures) + : TargetFailure(targetName) + , m_testCaseFailures(AZStd::move(testFailures)) + { + for (const auto& testCase : m_testCaseFailures) + { + m_numTestFailures += testCase.GetTestFailures().size(); + } + } + + size_t TestRunFailure::GetNumTestFailures() const + { + return m_numTestFailures; + } + + const AZStd::vector& TestRunFailure::GetTestCaseFailures() const + { + return m_testCaseFailures; + } + + SequenceFailure::SequenceFailure( + AZStd::vector&& executionFailures, + AZStd::vector&& testRunFailures, + AZStd::vector&& timedOutTests, + AZStd::vector&& unexecutionTests) + : m_executionFailures(AZStd::move(executionFailures)) + , m_testRunFailures(testRunFailures) + , m_timedOutTests(AZStd::move(timedOutTests)) + , m_unexecutedTests(AZStd::move(unexecutionTests)) + { + } + + const AZStd::vector& SequenceFailure::GetExecutionFailures() const + { + return m_executionFailures; + } + + const AZStd::vector& SequenceFailure::GetTestRunFailures() const + { + return m_testRunFailures; + } + + const AZStd::vector& SequenceFailure::GetTimedOutTests() const + { + return m_timedOutTests; + } + + const AZStd::vector& SequenceFailure::GetUnexecutedTests() const + { + return m_unexecutedTests; + } + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestRun.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestRun.cpp new file mode 100644 index 0000000000..dfc9f9d46c --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestRun.cpp @@ -0,0 +1,40 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +namespace TestImpact +{ + namespace Client + { + TestRun::TestRun(const AZStd::string& name, TestRunResult result, AZStd::chrono::milliseconds duration) + : m_targetName(name) + , m_result(result) + , m_duration(duration) + { + } + + const AZStd::string& TestRun::GetTargetName() const + { + return m_targetName; + } + + AZStd::chrono::milliseconds TestRun::GetDuration() const + { + return m_duration; + } + + TestRunResult TestRun::GetResult() const + { + return m_result; + } + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestSelection.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestSelection.cpp new file mode 100644 index 0000000000..067ab53fd5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactClientTestSelection.cpp @@ -0,0 +1,56 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + namespace Client + { + TestRunSelection::TestRunSelection(const AZStd::vector& includedTests, const AZStd::vector& excludedTests) + : m_includedTestRuns(includedTests) + , m_excludedTestRuns(excludedTests) + { + } + + TestRunSelection::TestRunSelection(AZStd::vector&& includedTests, AZStd::vector&& excludedTests) + : m_includedTestRuns(AZStd::move(includedTests)) + , m_excludedTestRuns(AZStd::move(excludedTests)) + { + } + + const AZStd::vector& TestRunSelection::GetIncludededTestRuns() const + { + return m_includedTestRuns; + } + + const AZStd::vector& TestRunSelection::GetExcludedTestRuns() const + { + return m_excludedTestRuns; + } + + size_t TestRunSelection::GetNumIncludedTestRuns() const + { + return m_includedTestRuns.size(); + } + + size_t TestRunSelection::GetNumExcludedTestRuns() const + { + return m_excludedTestRuns.size(); + } + + size_t TestRunSelection::GetTotalNumTests() const + { + return GetNumIncludedTestRuns() + GetNumExcludedTestRuns(); + } + } // namespace Client +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactException.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactException.cpp new file mode 100644 index 0000000000..804ab26e4d --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactException.cpp @@ -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. + * + */ + +#include + +namespace TestImpact +{ + Exception::Exception(const AZStd::string& msg) + : m_msg(msg) + { + } + + Exception::Exception(const char* msg) + : m_msg(msg) + { + } + + const char* Exception::what() const noexcept + { + return m_msg.c_str(); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRepoPath.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRepoPath.cpp new file mode 100644 index 0000000000..86b9cbbed5 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRepoPath.cpp @@ -0,0 +1,102 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include + +namespace TestImpact +{ + RepoPath& RepoPath::operator=(const string_type& other) noexcept + { + m_path = AZ::IO::Path(other).MakePreferred(); + return *this; + } + + RepoPath& RepoPath::operator=(const value_type* other) noexcept + { + m_path = AZ::IO::Path(other).MakePreferred(); + return *this; + } + + RepoPath& RepoPath::operator=(const AZ::IO::Path& other) noexcept + { + m_path = AZ::IO::Path(other).MakePreferred(); + return *this; + } + + RepoPath operator/(const RepoPath& lhs, const AZ::IO::PathView& rhs) + { + RepoPath result(lhs); + result.m_path /= RepoPath(rhs).m_path; + return result; + } + + RepoPath operator/(const RepoPath& lhs, AZStd::string_view rhs) + { + RepoPath result(lhs); + result.m_path /= RepoPath(rhs).m_path; + return result; + } + + RepoPath operator/(const RepoPath& lhs, const RepoPath::value_type* rhs) + { + RepoPath result(lhs); + result.m_path /= RepoPath(rhs).m_path; + return result; + } + + RepoPath operator/(const RepoPath& lhs, const RepoPath& rhs) + { + RepoPath result(lhs); + result.m_path /= rhs.m_path; + return result; + } + + RepoPath& RepoPath::operator/=(const AZ::IO::PathView& rhs) + { + m_path /= RepoPath(rhs).m_path; + return *this; + } + + + RepoPath& RepoPath::operator/=(AZStd::string_view rhs) + { + m_path /= RepoPath(rhs).m_path; + return *this; + } + + RepoPath& RepoPath::operator/=(const RepoPath::value_type* rhs) + { + m_path /= RepoPath(rhs).m_path; + return *this; + } + + RepoPath& RepoPath::operator/=(const RepoPath& rhs) + { + m_path /= rhs.m_path; + return *this; + } + + bool operator==(const RepoPath& lhs, const RepoPath& rhs) noexcept + { + return lhs.m_path.Compare(rhs.m_path) == 0; + } + + bool operator!=(const RepoPath& lhs, const RepoPath& rhs) noexcept + { + return lhs.m_path.Compare(rhs.m_path) != 0; + } + + bool operator<([[maybe_unused]] const RepoPath& lhs, [[maybe_unused]] const RepoPath& rhs) noexcept + { + return lhs.m_path.String() < rhs.m_path.String(); + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp new file mode 100644 index 0000000000..784a46ffa0 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntime.cpp @@ -0,0 +1,604 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + namespace + { + //! Simple helper class for tracking basic timing information. + class Timer + { + public: + Timer() + : m_startTime(AZStd::chrono::high_resolution_clock::now()) + { + } + + //! Returns the time elapsed (in milliseconds) since the timer was instantiated + AZStd::chrono::milliseconds Elapsed() + { + const auto endTime = AZStd::chrono::high_resolution_clock::now(); + return AZStd::chrono::duration_cast(endTime - m_startTime); + } + + private: + AZStd::chrono::high_resolution_clock::time_point m_startTime; + }; + + //! Handler for test run complete events. + class TestRunCompleteCallbackHandler + { + public: + TestRunCompleteCallbackHandler(AZStd::optional testCompleteCallback) + : m_testCompleteCallback(testCompleteCallback) + { + } + + void operator()(const TestEngineJob& testJob) + { + if (m_testCompleteCallback.has_value()) + { + (*m_testCompleteCallback) + (Client::TestRun(testJob.GetTestTarget()->GetName(), testJob.GetTestResult(), testJob.GetDuration())); + } + } + + private: + AZStd::optional m_testCompleteCallback; + }; + } + + //! Utility for concatenating two vectors. + template + AZStd::vector ConcatenateVectors(const AZStd::vector& v1, const AZStd::vector& v2) + { + AZStd::vector result; + result.reserve(v1.size() + v2.size()); + result.insert(result.end(), v1.begin(), v1.end()); + result.insert(result.end(), v2.begin(), v2.end()); + return result; + } + + Runtime::Runtime( + RuntimeConfig&& config, + SuiteType suiteFilter, + Policy::ExecutionFailure executionFailurePolicy, + Policy::FailedTestCoverage failedTestCoveragePolicy, + Policy::TestFailure testFailurePolicy, + Policy::IntegrityFailure integrationFailurePolicy, + Policy::TestSharding testShardingPolicy, + Policy::TargetOutputCapture targetOutputCapture, + AZStd::optional maxConcurrency) + : m_config(AZStd::move(config)) + , m_suiteFilter(suiteFilter) + , m_executionFailurePolicy(executionFailurePolicy) + , m_failedTestCoveragePolicy(failedTestCoveragePolicy) + , m_testFailurePolicy(testFailurePolicy) + , m_integrationFailurePolicy(integrationFailurePolicy) + , m_testShardingPolicy(testShardingPolicy) + , m_targetOutputCapture(targetOutputCapture) + , m_maxConcurrency(maxConcurrency.value_or(AZStd::thread::hardware_concurrency())) + { + // Construct the dynamic dependency map from the build target descriptors + m_dynamicDependencyMap = ConstructDynamicDependencyMap(suiteFilter, m_config.m_buildTargetDescriptor, m_config.m_testTargetMeta); + + // Construct the test selector and prioritizer from the dependency graph data (NOTE: currently not implemented) + m_testSelectorAndPrioritizer = AZStd::make_unique(m_dynamicDependencyMap.get(), DependencyGraphDataMap{}); + + // Construct the target exclude list from the target configuration data + m_testTargetExcludeList = ConstructTestTargetExcludeList(m_dynamicDependencyMap->GetTestTargetList(), m_config.m_target.m_excludedTestTargets); + + // Construct the test engine with the workspace path and launcher binaries + m_testEngine = AZStd::make_unique( + m_config.m_repo.m_root, + m_config.m_target.m_outputDirectory, + m_config.m_workspace.m_active.m_enumerationCacheDirectory, + m_config.m_workspace.m_temp.m_artifactDirectory, + m_config.m_testEngine.m_testRunner.m_binary, + m_config.m_testEngine.m_instrumentation.m_binary, + m_maxConcurrency); + + try + { + // Populate the dynamic dependency map with the existing source coverage data (if any) + m_sparTIAFile = m_config.m_workspace.m_active.m_sparTIAFiles[static_cast(m_suiteFilter)].String(); + const auto tiaDataRaw = ReadFileContents(m_sparTIAFile); + const auto tiaData = DeserializeSourceCoveringTestsList(tiaDataRaw); + if (tiaData.GetNumSources()) + { + m_dynamicDependencyMap->ReplaceSourceCoverage(tiaData); + m_hasImpactAnalysisData = true; + + // Enumerate new test targets + const auto testTargetsWithNoEnumeration = m_dynamicDependencyMap->GetNotCoveringTests(); + if (!testTargetsWithNoEnumeration.empty()) + { + m_testEngine->UpdateEnumerationCache( + testTargetsWithNoEnumeration, + Policy::ExecutionFailure::Ignore, + Policy::TestFailure::Continue, + AZStd::nullopt, + AZStd::nullopt, + AZStd::nullopt); + } + } + } + catch (const DependencyException& e) + { + if (integrationFailurePolicy == Policy::IntegrityFailure::Abort) + { + throw RuntimeException(e.what()); + } + } + catch ([[maybe_unused]]const Exception& e) + { + AZ_Printf("TestImpactRuntime", + AZStd::string::format( + "No test impact analysis data found for suite '%s' at %s\n", GetSuiteTypeName(m_suiteFilter).c_str(), m_sparTIAFile.c_str()).c_str()); + } + } + + Runtime::~Runtime() = default; + + void Runtime::EnumerateMutatedTestTargets(const ChangeDependencyList& changeDependencyList) + { + AZStd::vector testTargets; + const auto addMutatedTestTargetsToEnumerationList = [this, &testTargets](const AZStd::vector& sourceDependencies) + { + for (const auto& sourceDependency : sourceDependencies) + { + for (const auto& parentTarget : sourceDependency.GetParentTargets()) + { + AZStd::visit([&testTargets]([[maybe_unused]] auto&& target) + { + if constexpr (IsTestTarget) + { + testTargets.push_back(target); + } + }, parentTarget.GetTarget()); + } + } + }; + + // Gather all of the test targets that have had any of their sources modified + addMutatedTestTargetsToEnumerationList(changeDependencyList.GetCreateSourceDependencies()); + addMutatedTestTargetsToEnumerationList(changeDependencyList.GetUpdateSourceDependencies()); + addMutatedTestTargetsToEnumerationList(changeDependencyList.GetDeleteSourceDependencies()); + + // Enumerate the mutated test targets to ensure their enumeration caches are up to date + if (!testTargets.empty()) + { + m_testEngine->UpdateEnumerationCache( + testTargets, + Policy::ExecutionFailure::Ignore, + Policy::TestFailure::Continue, + AZStd::nullopt, + AZStd::nullopt, + AZStd::nullopt); + } + } + + AZStd::pair, AZStd::vector> Runtime::SelectCoveringTestTargetsAndUpdateEnumerationCache( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy) + { + AZStd::vector discardedTestTargets; + + // Select and prioritize the test targets pertinent to this change list + const auto changeDependencyList = m_dynamicDependencyMap->ApplyAndResoveChangeList(changeList); + const auto selectedTestTargets = m_testSelectorAndPrioritizer->SelectTestTargets(changeDependencyList, testPrioritizationPolicy); + + // Populate a set with the selected test targets so that we can infer the discarded test target not selected for this change list + const AZStd::unordered_set selectedTestTargetSet(selectedTestTargets.begin(), selectedTestTargets.end()); + + // Update the enumeration caches of mutated targets regardless of the current sharding policy + EnumerateMutatedTestTargets(changeDependencyList); + + // The test targets in the main list not in the selected test target set are the test targets not selected for this change list + for (const auto& testTarget : m_dynamicDependencyMap->GetTestTargetList().GetTargets()) + { + if (!selectedTestTargetSet.contains(&testTarget)) + { + discardedTestTargets.push_back(&testTarget); + } + } + + return { selectedTestTargets, discardedTestTargets }; + } + + AZStd::pair, AZStd::vector> Runtime::SelectTestTargetsByExcludeList( + AZStd::vector testTargets) const + { + AZStd::vector includedTestTargets; + AZStd::vector excludedTestTargets; + + if (m_testTargetExcludeList.empty()) + { + return { testTargets, {} }; + } + + for (const auto& testTarget : testTargets) + { + if (!m_testTargetExcludeList.contains(testTarget)) + { + includedTestTargets.push_back(testTarget); + } + else + { + excludedTestTargets.push_back(testTarget); + } + } + + return { includedTestTargets, excludedTestTargets }; + } + + void Runtime::ClearDynamicDependencyMapAndRemoveExistingFile() + { + m_dynamicDependencyMap->ClearAllSourceCoverage(); + DeleteFile(m_sparTIAFile); + } + + SourceCoveringTestsList Runtime::CreateSourceCoveringTestFromTestCoverages(const AZStd::vector& jobs) + { + AZStd::unordered_map> coverage; + for (const auto& job : jobs) + { + // First we must remove any existing coverage for the test target so as to not end up with source remnants from previous + // coverage that is no longer covered by this revision of the test target + m_dynamicDependencyMap->RemoveTestTargetFromSourceCoverage(job.GetTestTarget()); + + // Next we will update the coverage of test targets that completed (with or without failures), unless the failed test coverage + // policy dictates we should instead discard the coverage of test targets with failing tests + const auto testResult = job.GetTestResult(); + + if (m_failedTestCoveragePolicy == Policy::FailedTestCoverage::Discard && testResult == Client::TestRunResult::TestFailures) + { + // Discard the coverage for this job + continue; + } + + if (testResult == Client::TestRunResult::AllTestsPass || testResult == Client::TestRunResult::TestFailures) + { + if (testResult == Client::TestRunResult::AllTestsPass) + { + // Passing tests should have coverage data, otherwise something is very wrong + AZ_TestImpact_Eval( + job.GetTestCoverge().has_value(), + RuntimeException, + AZStd::string::format( + "Test target '%s' completed its test run successfully but produced no coverage data", + job.GetTestTarget()->GetName().c_str())); + } + + if (!job.GetTestCoverge().has_value()) + { + // When a test run completes with failing tests but produces no coverage artifact that's typically a sign of the + // test aborting due to an unhandled exception, in which case ignore it and let it be picked up in the failure report + continue; + } + + for (const auto& source : job.GetTestCoverge().value().GetSourcesCovered()) + { + coverage[source.String()].insert(job.GetTestTarget()->GetName()); + } + } + } + + AZStd::vector sourceCoveringTests; + sourceCoveringTests.reserve(coverage.size()); + for (auto&& [source, testTargets] : coverage) + { + if (const auto sourcePath = RepoPath(source); + sourcePath.IsRelativeTo(m_config.m_repo.m_root)) + { + sourceCoveringTests.push_back( + SourceCoveringTests(RepoPath(sourcePath.LexicallyRelative(m_config.m_repo.m_root)), AZStd::move(testTargets))); + } + else + { + AZ_Warning("TestImpact", false, "Ignoring source, source it outside of repo: '%s'", sourcePath.c_str()); + } + } + + return SourceCoveringTestsList(AZStd::move(sourceCoveringTests)); + } + + void Runtime::UpdateAndSerializeDynamicDependencyMap(const AZStd::vector& jobs) + { + const auto sourceCoverageTestsList = CreateSourceCoveringTestFromTestCoverages(jobs); + if (!sourceCoverageTestsList.GetNumSources()) + { + return; + } + + m_dynamicDependencyMap->ReplaceSourceCoverage(sourceCoverageTestsList); + const auto sparTIA = m_dynamicDependencyMap->ExportSourceCoverage(); + const auto sparTIAData = SerializeSourceCoveringTestsList(sparTIA); + WriteFileContents(sparTIAData, m_sparTIAFile); + m_hasImpactAnalysisData = true; + } + + TestSequenceResult Runtime::RegularTestSequence( + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceEndCallback, + AZStd::optional testCompleteCallback) + { + Timer timer; + AZStd::vector includedTestTargets; + AZStd::vector excludedTestTargets; + + // Separate the test targets into those that are excluded by either the test filter or exclusion list and those that are not + for (const auto& testTarget : m_dynamicDependencyMap->GetTestTargetList().GetTargets()) + { + if (!m_testTargetExcludeList.contains(&testTarget)) + { + includedTestTargets.push_back(&testTarget); + } + else + { + // Test targets on the exclude list are excluded + excludedTestTargets.push_back(&testTarget); + } + } + + // Sequence start callback + if (testSequenceStartCallback.has_value()) + { + (*testSequenceStartCallback)(Client::TestRunSelection(ExtractTestTargetNames(includedTestTargets), ExtractTestTargetNames(excludedTestTargets))); + } + + const auto [result, testJobs] = m_testEngine->RegularRun( + includedTestTargets, + m_testShardingPolicy, + m_executionFailurePolicy, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + if (testSequenceEndCallback.has_value()) + { + (*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed()); + } + + return result; + } + + TestSequenceResult Runtime::ImpactAnalysisTestSequence( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy, + Policy::DynamicDependencyMap dynamicDependencyMapPolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceEndCallback, + AZStd::optional testCompleteCallback) + { + Timer timer; + + // Draft in the test targets that have no coverage entries in the dynamic dependency map + AZStd::vector draftedTestTargets = m_dynamicDependencyMap->GetNotCoveringTests(); + + // The test targets that were selected for the change list by the dynamic dependency map and the test targets that were not + auto [selectedTestTargets, discardedTestTargets] = SelectCoveringTestTargetsAndUpdateEnumerationCache(changeList, testPrioritizationPolicy); + + // The subset of selected test targets that are not on the configuration's exclude list and those that are + auto [includedSelectedTestTargets, excludedSelectedTestTargets] = SelectTestTargetsByExcludeList(selectedTestTargets); + + // We present to the client the included selected test targets and the drafted test targets as distinct sets but internally + // we consider the concatenated set of the two the actual set of tests to run + AZStd::vector testTargetsToRun = ConcatenateVectors(includedSelectedTestTargets, draftedTestTargets); + + if (testSequenceStartCallback.has_value()) + { + (*testSequenceStartCallback)( + Client::TestRunSelection(ExtractTestTargetNames(includedSelectedTestTargets), ExtractTestTargetNames(excludedSelectedTestTargets)), + ExtractTestTargetNames(discardedTestTargets), + ExtractTestTargetNames(draftedTestTargets)); + } + + if (dynamicDependencyMapPolicy == Policy::DynamicDependencyMap::Update) + { + const auto [result, testJobs] = m_testEngine->InstrumentedRun( + testTargetsToRun, + m_testShardingPolicy, + m_executionFailurePolicy, + Policy::IntegrityFailure::Continue, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + UpdateAndSerializeDynamicDependencyMap(testJobs); + + if (testSequenceEndCallback.has_value()) + { + (*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed()); + } + + return result; + } + else + { + const auto [result, testJobs] = m_testEngine->RegularRun( + testTargetsToRun, + m_testShardingPolicy, + m_executionFailurePolicy, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + if (testSequenceEndCallback.has_value()) + { + (*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed()); + } + + return result; + } + } + + AZStd::pair Runtime::SafeImpactAnalysisTestSequence( + const ChangeList& changeList, + Policy::TestPrioritization testPrioritizationPolicy, + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceEndCallback, + AZStd::optional testCompleteCallback) + { + Timer timer; + + // Draft in the test targets that have no coverage entries in the dynamic dependency map + AZStd::vector draftedTestTargets = m_dynamicDependencyMap->GetNotCoveringTests(); + + // The test targets that were selected for the change list by the dynamic dependency map and the test targets that were not + auto [selectedTestTargets, discardedTestTargets] = SelectCoveringTestTargetsAndUpdateEnumerationCache(changeList, testPrioritizationPolicy); + + // The subset of selected test targets that are not on the configuration's exclude list and those that are + auto [includedSelectedTestTargets, excludedSelectedTestTargets] = SelectTestTargetsByExcludeList(selectedTestTargets); + + // The subset of discarded test targets that are not on the configuration's exclude list and those that are + auto [includedDiscardedTestTargets, excludedDiscardedTestTargets] = SelectTestTargetsByExcludeList(discardedTestTargets); + + // We present to the client the included selected test targets and the drafted test targets as distinct sets but internally + // we consider the concatenated set of the two the actual set of tests to run + AZStd::vector testTargetsToRun = ConcatenateVectors(includedSelectedTestTargets, draftedTestTargets); + + if (testSequenceStartCallback.has_value()) + { + (*testSequenceStartCallback)( + Client::TestRunSelection(ExtractTestTargetNames(includedSelectedTestTargets), ExtractTestTargetNames(excludedSelectedTestTargets)), + Client::TestRunSelection(ExtractTestTargetNames(includedDiscardedTestTargets), ExtractTestTargetNames(excludedDiscardedTestTargets)), + ExtractTestTargetNames(draftedTestTargets)); + } + + // Impact analysis run of the selected test targets + const auto [selectedResult, selectedTestJobs] = m_testEngine->InstrumentedRun( + testTargetsToRun, + m_testShardingPolicy, + m_executionFailurePolicy, + Policy::IntegrityFailure::Continue, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + const auto selectedDuraton = timer.Elapsed(); + + // Carry the remaining global sequence time over to the discarded test run + if (globalTimeout.has_value()) + { + const auto elapsed = timer.Elapsed(); + globalTimeout = elapsed < globalTimeout.value() ? globalTimeout.value() - elapsed : AZStd::chrono::milliseconds(0); + } + + // Regular run of the discarded test targets + const auto [discardedResult, discardedTestJobs] = m_testEngine->RegularRun( + includedDiscardedTestTargets, + m_testShardingPolicy, + m_executionFailurePolicy, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + const auto discardedDuraton = timer.Elapsed(); + + if (testSequenceEndCallback.has_value()) + { + (*testSequenceEndCallback)( + GenerateSequenceFailureReport(selectedTestJobs), + GenerateSequenceFailureReport(discardedTestJobs), + selectedDuraton, + discardedDuraton); + } + + UpdateAndSerializeDynamicDependencyMap(selectedTestJobs); + return { selectedResult, discardedResult }; + } + + TestSequenceResult Runtime::SeededTestSequence( + AZStd::optional testTargetTimeout, + AZStd::optional globalTimeout, + AZStd::optional testSequenceStartCallback, + AZStd::optional testSequenceEndCallback, + AZStd::optional testCompleteCallback) + { + Timer timer; + AZStd::vector includedTestTargets; + AZStd::vector excludedTestTargets; + + for (const auto& testTarget : m_dynamicDependencyMap->GetTestTargetList().GetTargets()) + { + if (!m_testTargetExcludeList.contains(&testTarget)) + { + includedTestTargets.push_back(&testTarget); + } + else + { + excludedTestTargets.push_back(&testTarget); + } + } + + if (testSequenceStartCallback.has_value()) + { + (*testSequenceStartCallback)(Client::TestRunSelection(ExtractTestTargetNames(includedTestTargets), ExtractTestTargetNames(excludedTestTargets))); + } + + const auto [result, testJobs] = m_testEngine->InstrumentedRun( + includedTestTargets, + m_testShardingPolicy, + m_executionFailurePolicy, + Policy::IntegrityFailure::Continue, + m_testFailurePolicy, + m_targetOutputCapture, + testTargetTimeout, + globalTimeout, + TestRunCompleteCallbackHandler(testCompleteCallback)); + + if (testSequenceEndCallback.has_value()) + { + (*testSequenceEndCallback)(GenerateSequenceFailureReport(testJobs), timer.Elapsed()); + } + + ClearDynamicDependencyMapAndRemoveExistingFile(); + UpdateAndSerializeDynamicDependencyMap(testJobs); + + return result; + } + + bool Runtime::HasImpactAnalysisData() const + { + return m_hasImpactAnalysisData; + } +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.cpp b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.cpp new file mode 100644 index 0000000000..68482ab721 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.cpp @@ -0,0 +1,85 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + TestTargetMetaMap ReadTestTargetMetaMapFile(SuiteType suiteFilter, const RepoPath& testTargetMetaConfigFile) + { + const auto masterTestListData = ReadFileContents(testTargetMetaConfigFile); + return TestTargetMetaMapFactory(masterTestListData, suiteFilter); + } + + AZStd::vector ReadBuildTargetDescriptorFiles(const BuildTargetDescriptorConfig& buildTargetDescriptorConfig) + { + AZStd::vector buildTargetDescriptors; + for (const auto& buildTargetDescriptorFile : std::filesystem::directory_iterator(buildTargetDescriptorConfig.m_mappingDirectory.c_str())) + { + const auto buildTargetDescriptorContents = ReadFileContents(buildTargetDescriptorFile.path().string().c_str()); + auto buildTargetDescriptor = TestImpact::BuildTargetDescriptorFactory( + buildTargetDescriptorContents, + buildTargetDescriptorConfig.m_staticInclusionFilters, + buildTargetDescriptorConfig.m_inputInclusionFilters, + buildTargetDescriptorConfig.m_inputOutputPairer); + buildTargetDescriptors.emplace_back(AZStd::move(buildTargetDescriptor)); + } + + return buildTargetDescriptors; + } + + AZStd::unique_ptr ConstructDynamicDependencyMap( + SuiteType suiteFilter, + const BuildTargetDescriptorConfig& buildTargetDescriptorConfig, + const TestTargetMetaConfig& testTargetMetaConfig) + { + auto testTargetmetaMap = ReadTestTargetMetaMapFile(suiteFilter, testTargetMetaConfig.m_metaFile); + auto buildTargetDescriptors = ReadBuildTargetDescriptorFiles(buildTargetDescriptorConfig); + auto buildTargets = CompileTargetDescriptors(AZStd::move(buildTargetDescriptors), AZStd::move(testTargetmetaMap)); + auto&& [productionTargets, testTargets] = buildTargets; + return AZStd::make_unique(AZStd::move(productionTargets), AZStd::move(testTargets)); + } + + AZStd::unordered_set ConstructTestTargetExcludeList( + const TestTargetList& testTargets, const AZStd::vector& excludedTestTargets) + { + AZStd::unordered_set testTargetExcludeList; + for (const auto& testTargetName : excludedTestTargets) + { + if (const auto* testTarget = testTargets.GetTarget(testTargetName); testTarget != nullptr) + { + testTargetExcludeList.insert(testTarget); + } + } + + return testTargetExcludeList; + } + + AZStd::vector ExtractTestTargetNames(const AZStd::vector testTargets) + { + AZStd::vector testNames; + AZStd::transform(testTargets.begin(), testTargets.end(), AZStd::back_inserter(testNames), [](const TestTarget* testTarget) + { + return testTarget->GetName(); + }); + + return testNames; + } +} // namespace TestImpact diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.h b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.h new file mode 100644 index 0000000000..807911d854 --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/Source/TestImpactRuntimeUtils.h @@ -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. + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace TestImpact +{ + //! Construct a dynamic dependency map from the build target descriptors and test target metas. + AZStd::unique_ptr ConstructDynamicDependencyMap( + SuiteType suiteFilter, + const BuildTargetDescriptorConfig& buildTargetDescriptorConfig, + const TestTargetMetaConfig& testTargetMetaConfig); + + //! Constructs the resolved test target exclude list from the specified list of targets and unresolved test target exclude list. + AZStd::unordered_set ConstructTestTargetExcludeList( + const TestTargetList& testTargets, + const AZStd::vector& excludedTestTargets); + + //! Extracts the name information from the specified test targets. + AZStd::vector ExtractTestTargetNames(const AZStd::vector testTargets); + + //! Generates a test run failure report from the specified test engine job information. + //! @tparam TestJob The test engine job type. + template + Client::TestRunFailure GenerateTestRunFailure(const TestJob& testJob) + { + if (testJob.GetTestRun().has_value()) + { + AZStd::vector testCaseFailures; + for (const auto& testSuite : testJob.GetTestRun()->GetTestSuites()) + { + AZStd::vector testFailures; + for (const auto& testCase : testSuite.m_tests) + { + if (testCase.m_result.value_or(TestRunResult::Passed) == TestRunResult::Failed) + { + testFailures.push_back(Client::TestFailure(testCase.m_name, "No error message retrieved")); + } + } + + if (!testFailures.empty()) + { + testCaseFailures.push_back(Client::TestCaseFailure(testSuite.m_name, AZStd::move(testFailures))); + } + } + + return Client::TestRunFailure(Client::TestRunFailure(testJob.GetTestTarget()->GetName(), AZStd::move(testCaseFailures))); + } + else + { + return Client::TestRunFailure(testJob.GetTestTarget()->GetName(), { }); + } + } + + //! Generates a sequence failure report from the specified list of test engine jobs. + //! @tparam TestJob The test engine job type. + template + Client::SequenceFailure GenerateSequenceFailureReport(const AZStd::vector& testJobs) + { + AZStd::vector executionFailures; + AZStd::vector testRunFailures; + AZStd::vector timedOutTestRuns; + AZStd::vector unexecutedTestRuns; + + for (const auto& testJob : testJobs) + { + switch (testJob.GetTestResult()) + { + case Client::TestRunResult::FailedToExecute: + { + executionFailures.push_back(Client::ExecutionFailure(testJob.GetTestTarget()->GetName(), testJob.GetCommandString())); + break; + } + case Client::TestRunResult::NotRun: + { + unexecutedTestRuns.push_back(testJob.GetTestTarget()->GetName()); + break; + } + case Client::TestRunResult::Timeout: + { + timedOutTestRuns.push_back(testJob.GetTestTarget()->GetName()); + break; + } + case Client::TestRunResult::AllTestsPass: + { + break; + } + case Client::TestRunResult::TestFailures: + { + testRunFailures.push_back(GenerateTestRunFailure(testJob)); + break; + } + default: + { + throw Exception( + AZStd::string::format("Unexpected client test run result: %u", static_cast(testJob.GetTestResult()))); + } + } + } + + return Client::SequenceFailure( + AZStd::move(executionFailures), + AZStd::move(testRunFailures), + AZStd::move(timedOutTestRuns), + AZStd::move(unexecutedTestRuns)); + } +} diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_files.cmake b/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_files.cmake index 74c7c84dcc..250894464b 100644 --- a/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_files.cmake +++ b/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_files.cmake @@ -10,5 +10,123 @@ # set(FILES - Source/Dummy.cpp + Include/TestImpactFramework/TestImpactException.h + Include/TestImpactFramework/TestImpactRepoPath.h + Include/TestImpactFramework/TestImpactRuntime.h + Include/TestImpactFramework/TestImpactRuntimeException.h + Include/TestImpactFramework/TestImpactConfiguration.h + Include/TestImpactFramework/TestImpactConfigurationException.h + Include/TestImpactFramework/TestImpactChangelist.h + Include/TestImpactFramework/TestImpactChangelistSerializer.h + Include/TestImpactFramework/TestImpactChangelistException.h + Include/TestImpactFramework/TestImpactTestSequence.h + Include/TestImpactFramework/TestImpactClientTestSelection.h + Include/TestImpactFramework/TestImpactClientTestRun.h + Include/TestImpactFramework/TestImpactClientFailureReport.h + Include/TestImpactFramework/TestImpactFileUtils.h + Source/Artifact/TestImpactArtifactException.h + Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.cpp + Source/Artifact/Factory/TestImpactBuildTargetDescriptorFactory.h + Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.cpp + Source/Artifact/Factory/TestImpactTestEnumerationSuiteFactory.h + Source/Artifact/Factory/TestImpactTestRunSuiteFactory.cpp + Source/Artifact/Factory/TestImpactTestRunSuiteFactory.h + Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.cpp + Source/Artifact/Factory/TestImpactTestTargetMetaMapFactory.h + Source/Artifact/Factory/TestImpactModuleCoverageFactory.cpp + Source/Artifact/Factory/TestImpactModuleCoverageFactory.h + Source/Artifact/Static/TestImpactBuildTargetDescriptor.cpp + Source/Artifact/Static/TestImpactBuildTargetDescriptor.h + Source/Artifact/Static/TestImpactTargetDescriptorCompiler.cpp + Source/Artifact/Static/TestImpactTargetDescriptorCompiler.h + Source/Artifact/Static/TestImpactProductionTargetDescriptor.cpp + Source/Artifact/Static/TestImpactProductionTargetDescriptor.h + Source/Artifact/Static/TestImpactTestTargetMeta.h + Source/Artifact/Static/TestImpactTestTargetDescriptor.cpp + Source/Artifact/Static/TestImpactTestTargetDescriptor.h + Source/Artifact/Static/TestImpactDependencyGraphData.h + Source/Artifact/Dynamic/TestImpactTestEnumerationSuite.h + Source/Artifact/Dynamic/TestImpactTestRunSuite.h + Source/Artifact/Dynamic/TestImpactTestSuite.h + Source/Artifact/Dynamic/TestImpactCoverage.h + Source/Process/TestImpactProcess.cpp + Source/Process/TestImpactProcess.h + Source/Process/TestImpactProcessException.h + Source/Process/TestImpactProcessInfo.cpp + Source/Process/TestImpactProcessInfo.h + Source/Process/TestImpactProcessLauncher.h + Source/Process/JobRunner/TestImpactProcessJob.h + Source/Process/JobRunner/TestImpactProcessJobInfo.h + Source/Process/JobRunner/TestImpactProcessJobMeta.cpp + Source/Process/JobRunner/TestImpactProcessJobMeta.h + Source/Process/JobRunner/TestImpactProcessJobRunner.h + Source/Process/Scheduler/TestImpactProcessScheduler.cpp + Source/Process/Scheduler/TestImpactProcessScheduler.h + Source/Dependency/TestImpactDynamicDependencyMap.cpp + Source/Dependency/TestImpactDynamicDependencyMap.h + Source/Dependency/TestImpactChangeDependencyList.cpp + Source/Dependency/TestImpactChangeDependencyList.h + Source/Dependency/TestImpactDependencyException.h + Source/Dependency/TestImpactSourceDependency.h + Source/Dependency/TestImpactSourceDependency.cpp + Source/Dependency/TestImpactTestSelectorAndPrioritizer.h + Source/Dependency/TestImpactTestSelectorAndPrioritizer.cpp + Source/Dependency/TestImpactSourceCoveringTestsList.h + Source/Dependency/TestImpactSourceCoveringTestsList.cpp + Source/Dependency/TestImpactSourceCoveringTestsSerializer.cpp + Source/Dependency/TestImpactSourceCoveringTestsSerializer.h + Source/Target/TestImpactBuildTarget.cpp + Source/Target/TestImpactBuildTarget.h + Source/Target/TestImpactBuildTargetList.h + Source/Target/TestImpactProductionTarget.cpp + Source/Target/TestImpactProductionTarget.h + Source/Target/TestImpactProductionTargetList.h + Source/Target/TestImpactTargetException.h + Source/Target/TestImpactTestTarget.cpp + Source/Target/TestImpactTestTarget.h + Source/Target/TestImpactTestTargetList.h + Source/TestEngine/Enumeration/TestImpactTestEnumeration.h + Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.cpp + Source/TestEngine/Enumeration/TestImpactTestEnumerationSerializer.h + Source/TestEngine/Enumeration/TestImpactTestEnumerator.cpp + Source/TestEngine/Enumeration/TestImpactTestEnumerator.h + Source/TestEngine/Run/TestImpactTestRunSerializer.cpp + Source/TestEngine/Run/TestImpactTestRunSerializer.h + Source/TestEngine/Run/TestImpactTestRunner.cpp + Source/TestEngine/Run/TestImpactTestRunner.h + Source/TestEngine/Run/TestImpactInstrumentedTestRunner.cpp + Source/TestEngine/Run/TestImpactInstrumentedTestRunner.h + Source/TestEngine/Run/TestImpactTestRun.cpp + Source/TestEngine/Run/TestImpactTestRun.h + Source/TestEngine/Run/TestImpactTestRunJobData.cpp + Source/TestEngine/Run/TestImpactTestRunJobData.h + Source/TestEngine/Run/TestImpactTestCoverage.cpp + Source/TestEngine/Run/TestImpactTestCoverage.h + Source/TestEngine/JobRunner/TestImpactTestJobRunner.h + Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.cpp + Source/TestEngine/JobRunner/TestImpactTestJobInfoGenerator.h + Source/TestEngine/JobRunner/TestImpactTestTargetExtension.h + Source/TestEngine/TestImpactTestEngineJobFailure.cpp + Source/TestEngine/TestImpactTestEngineJobFailure.h + Source/TestEngine/TestImpactTestSuiteContainer.h + Source/TestEngine/TestImpactTestEngine.cpp + Source/TestEngine/TestImpactTestEngine.h + Source/TestEngine/TestImpactTestEngineJob.cpp + Source/TestEngine/TestImpactTestEngineJob.h + Source/TestEngine/TestImpactTestEngineEnumeration.cpp + Source/TestEngine/TestImpactTestEngineEnumeration.h + Source/TestEngine/TestImpactTestEngineRegularRun.cpp + Source/TestEngine/TestImpactTestEngineRegularRun.h + Source/TestEngine/TestImpactTestEngineInstrumentedRun.cpp + Source/TestEngine/TestImpactTestEngineInstrumentedRun.h + Source/TestEngine/TestImpactTestEngineException.h + Source/TestImpactException.cpp + Source/TestImpactRuntime.cpp + Source/TestImpactRuntimeUtils.cpp + Source/TestImpactRuntimeUtils.h + Source/TestImpactClientTestSelection.cpp + Source/TestImpactClientTestRun.cpp + Source/TestImpactClientFailureReport.cpp + Source/TestImpactChangeListSerializer.cpp + Source/TestImpactRepoPath.cpp ) diff --git a/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_tests_files.cmake b/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_tests_files.cmake new file mode 100644 index 0000000000..5714be5dfb --- /dev/null +++ b/Code/Tools/TestImpactFramework/Runtime/Code/testimpactframework_runtime_tests_files.cmake @@ -0,0 +1,13 @@ +# +# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +# its licensors. +# +# For complete copyright and license terms please see the LICENSE at the root of this +# distribution (the "License"). All use of this software is governed by the License, +# or, if provided, by the license below or the license accompanying this file. Do not +# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# + +set(FILES +) diff --git a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h index c892f86b66..ab03223323 100644 --- a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h +++ b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h @@ -46,6 +46,7 @@ namespace AWSCore void InitializeAWSDocActions(); void InitializeAWSGlobalDocsSubMenu(); void InitializeAWSFeatureGemActions(); + void AddSpaceForIcon(QMenu* menu); // AWSCoreEditorRequestBus interface implementation void SetAWSClientAuthEnabled() override; diff --git a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp index c319788547..27e8710cbc 100644 --- a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp +++ b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp @@ -35,6 +35,9 @@ namespace AWSCore { + + static constexpr int IconSize = 16; + AWSCoreEditorMenu::AWSCoreEditorMenu(const QString& text) : QMenu(text) , m_resourceMappingToolWatcher(nullptr) @@ -43,6 +46,7 @@ namespace AWSCore InitializeResourceMappingToolAction(); this->addSeparator(); InitializeAWSFeatureGemActions(); + AddSpaceForIcon(this); AWSCoreEditorRequestBus::Handler::BusConnect(); } @@ -136,6 +140,8 @@ namespace AWSCore globalDocsMenu->addAction(AddExternalLinkAction(AWSAndScriptCanvasActionText, AWSAndScriptCanvasUrl, ":/Notifications/link.svg")); globalDocsMenu->addAction(AddExternalLinkAction(AWSAndComponentsActionText, AWSAndComponentsUrl, ":/Notifications/link.svg")); globalDocsMenu->addAction(AddExternalLinkAction(CallAWSResourcesActionText, CallAWSResourcesUrl, ":/Notifications/link.svg")); + + AddSpaceForIcon(globalDocsMenu); } void AWSCoreEditorMenu::InitializeAWSFeatureGemActions() @@ -170,6 +176,8 @@ namespace AWSCore AWSClientAuthPlatformSpecificActionText, AWSClientAuthPlatformSpecificUrl, ":/Notifications/link.svg")); subMenu->addAction(AddExternalLinkAction( AWSClientAuthAPIReferenceActionText, AWSClientAuthAPIReferenceUrl, ":/Notifications/link.svg")); + + AddSpaceForIcon(subMenu); } void AWSCoreEditorMenu::SetAWSMetricsEnabled() @@ -197,7 +205,9 @@ namespace AWSCore [configFilePath](){ QDesktopServices::openUrl(QUrl::fromLocalFile(configFilePath.c_str())); }); + subMenu->addAction(settingsAction); + AddSpaceForIcon(subMenu); } QMenu* AWSCoreEditorMenu::SetAWSFeatureSubMenu(const AZStd::string& menuText) @@ -209,6 +219,7 @@ namespace AWSCore { QMenu* subMenu = new QMenu(QObject::tr(menuText.c_str())); subMenu->setIcon(QIcon(QString(":/Notifications/checkmark.svg"))); + subMenu->setProperty("noHover", true); this->insertMenu(*itr, subMenu); this->removeAction(*itr); return subMenu; @@ -216,4 +227,11 @@ namespace AWSCore } return nullptr; } + + void AWSCoreEditorMenu::AddSpaceForIcon(QMenu *menu) + { + QSize size = menu->sizeHint(); + size.setWidth(size.width() + IconSize); + menu->setFixedSize(size); + } } // namespace AWSCore diff --git a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h index 28c8c3d762..b8b6a8ccce 100644 --- a/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h +++ b/Gems/AWSMetrics/Code/Include/Private/MetricsManager.h @@ -122,29 +122,22 @@ namespace AWSMetrics //! @return Outcome of the operation. AZ::Outcome SendMetricsToFile(AZStd::shared_ptr metricsQueue); - //! Check whether the consumer should flush the metrics queue. - //! @return whether the limit is hit. - bool ShouldSendMetrics(); - //! Push metrics events to the front of the queue for retry. //! @param metricsEventsForRetry Metrics events for retry. void PushMetricsForRetry(MetricsQueue& metricsEventsForRetry); void SubmitLocalMetricsAsync(); - //////////////////////////////////////////// - // These data are protected by m_metricsMutex. - AZStd::mutex m_metricsMutex; - AZStd::chrono::system_clock::time_point m_lastSendMetricsTime; - MetricsQueue m_metricsQueue; - //////////////////////////////////////////// + AZStd::mutex m_metricsMutex; //!< Mutex to protect the metrics queue + MetricsQueue m_metricsQueue; //!< Queue fo buffering the metrics events - AZStd::mutex m_metricsFileMutex; //!< Local metrics file is protected by m_metricsFileMutex + AZStd::mutex m_metricsFileMutex; //!< Mutex to protect the local metrics file AZStd::atomic m_sendMetricsId;//!< Request ID for sending metrics - AZStd::thread m_consumerThread; //!< Thread to monitor and consume the metrics queue - AZStd::atomic m_consumerTerminated; + AZStd::thread m_monitorThread; //!< Thread to monitor and consume the metrics queue + AZStd::atomic m_monitorTerminated; + AZStd::binary_semaphore m_waitEvent; // Client Configurations. AZStd::unique_ptr m_clientConfiguration; diff --git a/Gems/AWSMetrics/Code/Source/MetricsManager.cpp b/Gems/AWSMetrics/Code/Source/MetricsManager.cpp index 2f4f1fb5e5..d48ec1a041 100644 --- a/Gems/AWSMetrics/Code/Source/MetricsManager.cpp +++ b/Gems/AWSMetrics/Code/Source/MetricsManager.cpp @@ -29,7 +29,7 @@ namespace AWSMetrics MetricsManager::MetricsManager() : m_clientConfiguration(AZStd::make_unique()) , m_clientIdProvider(IdentityProvider::CreateIdentityProvider()) - , m_consumerTerminated(true) + , m_monitorTerminated(true) , m_sendMetricsId(0) { } @@ -53,31 +53,27 @@ namespace AWSMetrics void MetricsManager::StartMetrics() { - if (!m_consumerTerminated) + if (!m_monitorTerminated) { // The background thread has been started. return; } - - m_consumerTerminated = false; - - AZStd::lock_guard lock(m_metricsMutex); - m_lastSendMetricsTime = AZStd::chrono::system_clock::now(); + m_monitorTerminated = false; // Start a separate thread to monitor and consume the metrics queue. // Avoid using the job system since the worker is long-running over multiple frames - m_consumerThread = AZStd::thread(AZStd::bind(&MetricsManager::MonitorMetricsQueue, this)); + m_monitorThread = AZStd::thread(AZStd::bind(&MetricsManager::MonitorMetricsQueue, this)); } void MetricsManager::MonitorMetricsQueue() { - while (!m_consumerTerminated) + // Continue to loop until the monitor is terminated. + while (!m_monitorTerminated) { - if (ShouldSendMetrics()) - { - // Flush the metrics queue when the accumulated metrics size or time period hits the limit - FlushMetricsAsync(); - } + // The thread will wake up either when the metrics event queue is full (try_acquire_for call returns true), + // or the flush period limit is hit (try_acquire_for call returns false). + m_waitEvent.try_acquire_for(AZStd::chrono::seconds(m_clientConfiguration->GetQueueFlushPeriodInSeconds())); + FlushMetricsAsync(); } } @@ -114,6 +110,12 @@ namespace AWSMetrics AZStd::lock_guard lock(m_metricsMutex); m_metricsQueue.AddMetrics(metricsEvent); + if (m_metricsQueue.GetSizeInBytes() >= m_clientConfiguration->GetMaxQueueSizeInBytes()) + { + // Flush the metrics queue when the accumulated metrics size hits the limit + m_waitEvent.release(); + } + return true; } @@ -348,9 +350,6 @@ namespace AWSMetrics void MetricsManager::FlushMetricsAsync() { AZStd::lock_guard lock(m_metricsMutex); - - m_lastSendMetricsTime = AZStd::chrono::system_clock::now(); - if (m_metricsQueue.GetNumMetrics() == 0) { return; @@ -363,34 +362,20 @@ namespace AWSMetrics SendMetricsAsync(metricsToFlush); } - bool MetricsManager::ShouldSendMetrics() - { - AZStd::lock_guard lock(m_metricsMutex); - - auto secondsSinceLastFlush = AZStd::chrono::duration_cast(AZStd::chrono::system_clock::now() - m_lastSendMetricsTime); - if (secondsSinceLastFlush >= AZStd::chrono::seconds(m_clientConfiguration->GetQueueFlushPeriodInSeconds()) || - m_metricsQueue.GetSizeInBytes() >= m_clientConfiguration->GetMaxQueueSizeInBytes()) - { - return true; - } - - return false; - } - void MetricsManager::ShutdownMetrics() { - if (m_consumerTerminated) + if (m_monitorTerminated) { return; } - // Terminate the consumer thread - m_consumerTerminated = true; - FlushMetricsAsync(); + // Terminate the monitor thread + m_monitorTerminated = true; + m_waitEvent.release(); - if (m_consumerThread.joinable()) + if (m_monitorThread.joinable()) { - m_consumerThread.join(); + m_monitorThread.join(); } } @@ -449,6 +434,12 @@ namespace AWSMetrics { AZStd::lock_guard lock(m_metricsMutex); m_metricsQueue.AddMetrics(offlineRecords[index]); + + if (m_metricsQueue.GetSizeInBytes() >= m_clientConfiguration->GetMaxQueueSizeInBytes()) + { + // Flush the metrics queue when the accumulated metrics size hits the limit + m_waitEvent.release(); + } } // Remove the local metrics file after reading all its content. diff --git a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp index 82385b8bdd..bd7ecb722e 100644 --- a/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp +++ b/Gems/AWSMetrics/Code/Tests/MetricsManagerTest.cpp @@ -355,6 +355,9 @@ namespace AWSMetrics TEST_F(MetricsManagerTest, FlushMetrics_NonEmptyQueue_Success) { + ResetClientConfig(true, (double)TestMetricsEventSizeInBytes * (MaxNumMetricsEvents + 1) / MbToBytes, + DefaultFlushPeriodInSeconds, 1); + for (int index = 0; index < MaxNumMetricsEvents; ++index) { AZStd::vector metricsAttributes; @@ -377,7 +380,7 @@ namespace AWSMetrics TEST_F(MetricsManagerTest, ResetOfflineRecordingStatus_ResubmitLocalMetrics_Success) { // Disable offline recording in the config file. - ResetClientConfig(false, 0.0, 0, 0); + ResetClientConfig(false, (double)TestMetricsEventSizeInBytes * 2 / MbToBytes, 0, 0); // Enable offline recording after initialize the metric manager. m_metricsManager->UpdateOfflineRecordingStatus(true); diff --git a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnail.cpp b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnail.cpp index dda4587588..600b012af0 100644 --- a/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnail.cpp +++ b/Gems/Atom/Asset/ImageProcessingAtom/Code/Source/Thumbnail/ImageThumbnail.cpp @@ -63,7 +63,7 @@ namespace ImageProcessingAtom void ImageThumbnail::LoadThread() { - AzToolsFramework::Thumbnailer::ThumbnailerRendererRequestBus::QueueEvent( + AzToolsFramework::Thumbnailer::ThumbnailerRendererRequestBus::Event( AZ::RPI::StreamingImageAsset::RTTI_Type(), &AzToolsFramework::Thumbnailer::ThumbnailerRendererRequests::RenderThumbnail, m_key, ImageThumbnailSize); diff --git a/Gems/Atom/Feature/Common/Assets/Config/Platform/Windows/DX12/PlatformLimits.azasset b/Gems/Atom/Feature/Common/Assets/Config/Platform/Windows/DX12/PlatformLimits.azasset index 7e71111ef1..3c88544e62 100644 --- a/Gems/Atom/Feature/Common/Assets/Config/Platform/Windows/DX12/PlatformLimits.azasset +++ b/Gems/Atom/Feature/Common/Assets/Config/Platform/Windows/DX12/PlatformLimits.azasset @@ -8,7 +8,7 @@ "$type": "DX12::PlatformLimitsDescriptor", "m_descriptorHeapLimits": { - "DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV": [16384, 262144], + "DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV": [1000000, 1000000], "DESCRIPTOR_HEAP_TYPE_SAMPLER": [2048, 2048], "DESCRIPTOR_HEAP_TYPE_RTV": [2048, 0], "DESCRIPTOR_HEAP_TYPE_DSV": [2048, 0] diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass index 9317fc6396..341893f1d5 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/LowEndPipeline.pass @@ -322,7 +322,14 @@ "Pass": "AuxGeomPass", "Attachment": "ColorInputOutput" } - } + }, + { + "LocalSlot": "DepthInputOutput", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "Depth" + } + } ] }, { diff --git a/Gems/Atom/Feature/Common/Assets/Passes/MainPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/MainPipeline.pass index b2e0cf088e..314d999e4d 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/MainPipeline.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/MainPipeline.pass @@ -427,6 +427,13 @@ "Pass": "DebugOverlayPass", "Attachment": "InputOutput" } + }, + { + "LocalSlot": "DepthInputOutput", + "AttachmentRef": { + "Pass": "DepthPrePass", + "Attachment": "Depth" + } } ] }, diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceBlur.pass b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceBlur.pass index 1d20382408..e2fde2d4ef 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceBlur.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceBlur.pass @@ -24,9 +24,9 @@ }, "ImageDescriptor": { "Format": "R16G16B16A16_FLOAT", - "MipLevels": "8", "SharedQueueMask": "Graphics" - } + }, + "GenerateFullMipChain": true } ], "Connections": [ diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceComposite.pass b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceComposite.pass index 80c4e8987b..5443c32406 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceComposite.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionScreenSpaceComposite.pass @@ -5,7 +5,7 @@ "ClassData": { "PassTemplate": { "Name": "ReflectionScreenSpaceCompositePassTemplate", - "PassClass": "FullScreenTriangle", + "PassClass": "ReflectionScreenSpaceCompositePass", "Slots": [ { "Name": "TraceInput", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/UI.pass b/Gems/Atom/Feature/Common/Assets/Passes/UI.pass index 569fe1d722..ac43f17c11 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/UI.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/UI.pass @@ -7,6 +7,23 @@ "Name": "UIPassTemplate", "PassClass": "RasterPass", "Slots": [ + { + "Name": "DepthInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil", + "LoadStoreAction": { + "ClearValue": { + "Type": "DepthStencil", + "Value": [ + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "LoadActionStencil": "Clear" + } + }, { "Name": "InputOutput", "SlotType": "InputOutput", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/UIParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/UIParent.pass index 0069e89401..4ae67b9b09 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/UIParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/UIParent.pass @@ -10,6 +10,11 @@ { "Name": "InputOutput", "SlotType": "InputOutput" + }, + { + "Name": "DepthInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" } ], "PassRequests": [ @@ -24,6 +29,13 @@ "Pass": "Parent", "Attachment": "InputOutput" } + }, + { + "LocalSlot": "DepthInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DepthInputOutput" + } } ], "PassData": { diff --git a/Gems/Atom/Feature/Common/Assets/Scripts/performance_metrics/timestamp_aggregator.py b/Gems/Atom/Feature/Common/Assets/Scripts/performance_metrics/timestamp_aggregator.py new file mode 100644 index 0000000000..0508395275 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Scripts/performance_metrics/timestamp_aggregator.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +""" +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. +""" + +from genericpath import isdir +from argparse import ArgumentParser +import json +from pathlib import Path +import os + +# this allows us to add additional data if necessary, e.g. frame_test_timestamps.json +is_timestamp_file = lambda file: file.name.startswith('frame') and file.name.endswith('_timestamps.json') +ns_to_ms = lambda time: time / 1e6 + +def main(logs_dir): + count = 0 + total = 0 + maximum = 0 + + print(f'Analyzing frame timestamp logs in {logs_dir}') + + # go through files in alphabetical order (remove sorted() if not necessary) + for file in sorted(logs_dir.iterdir(), key=lambda file: len(file.name)): + if file.is_dir() or not is_timestamp_file(file): + continue + + data = json.loads(file.read_text()) + entries = data['ClassData']['timestampEntries'] + timestamps = [entry['timestampResultInNanoseconds'] for entry in entries] + + frame_time = sum(timestamps) + frame_name = file.name.split('_')[0] + print(f'- Total time for frame {frame_name}: {ns_to_ms(frame_time)}ms') + + maximum = max(maximum, frame_time) + total += frame_time + count += 1 + + if count < 1: + print(f'No logs were found in {base_dir}') + exit(1) + + print(f'Avg. time across {count} frames: {ns_to_ms(total / count)}ms') + print(f'Max frame time: {ns_to_ms(maximum)}ms') + +if __name__ == '__main__': + parser = ArgumentParser(description='Gathers statistics from a group of pass timestamp logs') + parser.add_argument('path', help='Path to the directory containing the pass timestamp logs') + args = parser.parse_args() + + base_dir = Path(args.path) + if not base_dir.exists(): + raise FileNotFoundError('Invalid path provided') + main(base_dir) diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader index 95ffc36a11..08b1e7c298 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetCS.shader @@ -10,7 +10,5 @@ "type": "Compute" } ] - }, - "DisabledRHIBackends": ["metal"] - + } } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetSRG.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetSRG.azsli index 7ec5b43368..f4b1beeb3e 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetSRG.azsli +++ b/Gems/Atom/Feature/Common/Assets/Shaders/MorphTargets/MorphTargetSRG.azsli @@ -16,7 +16,10 @@ ShaderResourceGroup MorphTargetPassSrg : SRG_PerPass { - RWBuffer m_accumulatedDeltas; + //Since we do Interlocked atomic operations on this buffer it can not be RWBuffer due to broken MetalSL generation. + //It stems from the fact that typed buffers gets converted to textures and that breaks with atomic operations. + //In future we can handle this under the hood via our metal shader pipeline + RWStructuredBuffer m_accumulatedDeltas; } // This class represents the data that is passed to the morph target compute shader of an individual delta diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.azsl index df92e36f9a..4238392004 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHeatmap.azsl @@ -37,7 +37,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass Texture2D m_sceneLuminance; // This should be of size NUM_HISTOGRAM_BINS. - Buffer m_histogram; + StructuredBuffer m_histogram; Sampler LinearSampler { diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.azsl index 05cc870eea..ee95515a2a 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.azsl @@ -20,7 +20,11 @@ ShaderResourceGroup PassSrg : SRG_PerPass { Texture2D m_inputTexture; - RWBuffer m_outputTexture; + + //Since we do Interlocked atomic operations on this buffer it can not be RWBuffer due to broken MetalSL generation. + //It stems from the fact that typed buffers gets converted to textures and that breaks with atomic operations. + //In future we can handle this under the hood via our metal shader pipeline + RWStructuredBuffer m_outputTexture; } groupshared uint shared_histogramBins[NUM_HISTOGRAM_BINS]; diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader index 566144bab8..f3dd11e11a 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/PostProcessing/LuminanceHistogramGenerator.shader @@ -12,7 +12,5 @@ "type": "Compute" } ] - }, - "DisabledRHIBackends": ["metal"] - + } } diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.azsl index fa3885f180..c5c724e106 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.azsl +++ b/Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionScreenSpaceComposite.azsl @@ -37,6 +37,9 @@ ShaderResourceGroup PassSrg : SRG_PerPass AddressV = Clamp; AddressW = Clamp; }; + + // the max roughness mip level for sampling the previous frame image + uint m_maxMipLevel; } #include @@ -69,10 +72,6 @@ PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex) float4 positionWS = mul(ViewSrg::m_viewProjectionInverseMatrix, projectedPos); positionWS /= positionWS.w; - //float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); - //positionVS /= positionVS.w; - //float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); - // compute ray from camera to surface position float3 cameraToPositionWS = normalize(positionWS.xyz - ViewSrg::m_worldPosition); @@ -103,8 +102,7 @@ PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex) // compute the roughness mip to use in the previous frame image // remap the roughness mip into a lower range to more closely match the material roughness values const float MaxRoughness = 0.5f; - const float MaxRoughnessMip = 7; - float mip = saturate(roughness / MaxRoughness) * MaxRoughnessMip; + float mip = saturate(roughness / MaxRoughness) * PassSrg::m_maxMipLevel; // sample reflection value from the roughness mip float4 reflectionColor = float4(PassSrg::m_previousFrame.SampleLevel(PassSrg::LinearSampler, tracePrevUV, mip).rgb, 1.0f); diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli index 5a9e44bade..e5407d2d9e 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli +++ b/Gems/Atom/Feature/Common/Assets/Shaders/SkinnedMesh/LinearSkinningPassSRG.azsli @@ -16,7 +16,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass { - RWBuffer m_skinnedMeshOutputStream; + RWStructuredBuffer m_skinnedMeshOutputStream; } ShaderResourceGroup InstanceSrg : SRG_PerDraw diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessorInterface.h index 88faac1728..ce50cea17f 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessorInterface.h @@ -23,7 +23,9 @@ namespace AZ { Low, Medium, - High + High, + + Count }; //! This class provides general features and configuration for the diffuse global illumination environment, 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 0d61ef82d1..24c0658c61 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 @@ -95,6 +95,8 @@ namespace AZ TransformServiceFeatureProcessorInterface::ObjectId m_objectId; + Aabb m_aabb = Aabb::CreateNull(); + bool m_cullBoundsNeedsUpdate = false; bool m_cullableNeedsRebuild = false; bool m_objectSrgNeedsUpdate = true; @@ -160,6 +162,9 @@ namespace AZ Transform GetTransform(const MeshHandle& meshHandle) override; Vector3 GetNonUniformScale(const MeshHandle& meshHandle) override; + void SetLocalAabb(const MeshHandle& meshHandle, const AZ::Aabb& localAabb) override; + AZ::Aabb GetLocalAabb(const MeshHandle& meshHandle) const override; + void SetSortKey(const MeshHandle& meshHandle, RHI::DrawItemSortKey sortKey) override; RHI::DrawItemSortKey GetSortKey(const MeshHandle& meshHandle) 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 fb5bff5584..fdc6ea3cc8 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 @@ -86,6 +86,10 @@ namespace AZ virtual Transform GetTransform(const MeshHandle& meshHandle) = 0; //! Gets the non-uniform scale for a given mesh handle. virtual Vector3 GetNonUniformScale(const MeshHandle& meshHandle) = 0; + //! Sets the local space bbox for a given mesh handle. You don't need to call this for static models, only skinned/animated models + virtual void SetLocalAabb(const MeshHandle& meshHandle, const AZ::Aabb& localAabb) = 0; + //! Gets the local space bbox for a given mesh handle. Unless SetLocalAabb has been called before, this will be the bbox of the model asset + virtual AZ::Aabb GetLocalAabb(const MeshHandle& meshHandle) const = 0; //! Sets the sort key for a given mesh handle. virtual void SetSortKey(const MeshHandle& meshHandle, RHI::DrawItemSortKey sortKey) = 0; //! Gets the sort key for a given mesh handle. diff --git a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h index 418ee0cfb8..e6635906e9 100644 --- a/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Mocks/MockMeshFeatureProcessor.h @@ -33,6 +33,8 @@ namespace UnitTest MOCK_METHOD2(SetMaterialAssignmentMap, void(const MeshHandle&, const AZ::Render::MaterialAssignmentMap&)); MOCK_METHOD1(GetTransform, AZ::Transform(const MeshHandle&)); MOCK_METHOD1(GetNonUniformScale, AZ::Vector3(const MeshHandle&)); + MOCK_METHOD2(SetLocalAabb, void(const MeshHandle&, const AZ::Aabb&)); + MOCK_CONST_METHOD1(GetLocalAabb, AZ::Aabb(const MeshHandle&)); MOCK_METHOD2(SetSortKey, void (const MeshHandle&, AZ::RHI::DrawItemSortKey)); MOCK_METHOD1(GetSortKey, AZ::RHI::DrawItemSortKey(const MeshHandle&)); MOCK_METHOD2(SetLodOverride, void(const MeshHandle&, AZ::RPI::Cullable::LodOverride)); diff --git a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp index af28624357..1866da63e5 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp @@ -103,6 +103,7 @@ #include #include #include +#include #include #include @@ -283,6 +284,7 @@ namespace AZ // Add Reflection passes passSystem->AddPassCreator(Name("ReflectionScreenSpaceBlurPass"), &Render::ReflectionScreenSpaceBlurPass::Create); passSystem->AddPassCreator(Name("ReflectionScreenSpaceBlurChildPass"), &Render::ReflectionScreenSpaceBlurChildPass::Create); + passSystem->AddPassCreator(Name("ReflectionScreenSpaceCompositePass"), &Render::ReflectionScreenSpaceCompositePass::Create); passSystem->AddPassCreator(Name("ReflectionCopyFrameBufferPass"), &Render::ReflectionCopyFrameBufferPass::Create); // Add RayTracing pas diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index ae8280fc16..2f8f874f38 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -1070,7 +1072,18 @@ namespace AZ segment.m_pipelineViewTag = viewTag; if (!segment.m_view || segment.m_view->GetName() != viewName) { - segment.m_view = RPI::View::CreateView(viewName, RPI::View::UsageShadow); + RPI::View::UsageFlags usageFlags = RPI::View::UsageShadow; + + // if the shadow is rendering in an EnvironmentCubeMapPass it also needs to be a ReflectiveCubeMap view, + // to filter out shadows from objects that are excluded from the cubemap + RPI::PassClassFilter passFilter; + AZStd::vector cubeMapPasses = AZ::RPI::PassSystemInterface::Get()->FindPasses(passFilter); + if (!cubeMapPasses.empty()) + { + usageFlags |= RPI::View::UsageReflectiveCubeMap; + } + + segment.m_view = RPI::View::CreateView(viewName, usageFlags); } } } diff --git a/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.cpp index 5040665456..1c28e18e0e 100644 --- a/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.cpp @@ -43,6 +43,12 @@ namespace AZ void DiffuseGlobalIlluminationFeatureProcessor::SetQualityLevel(DiffuseGlobalIlluminationQualityLevel qualityLevel) { + if (qualityLevel >= DiffuseGlobalIlluminationQualityLevel::Count) + { + AZ_Assert(false, "SetQualityLevel called with invalid quality level [%d]", qualityLevel); + return; + } + m_qualityLevel = qualityLevel; UpdatePasses(); diff --git a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp index a064b61a1a..ae48c7defb 100644 --- a/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/DisplayMapper/DisplayMapperConfigurationDescriptor.cpp @@ -50,6 +50,8 @@ namespace AZ if (auto behaviorContext = azrtti_cast(context)) { + behaviorContext->Class("OutputDeviceTransformType"); + behaviorContext->Class("AcesParameterOverrides") ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Category, "render") @@ -58,6 +60,16 @@ namespace AZ ->Method("LoadPreset", &AcesParameterOverrides::LoadPreset) ->Property("overrideDefaults", BehaviorValueProperty(&AcesParameterOverrides::m_overrideDefaults)) ->Property("preset", BehaviorValueProperty(&AcesParameterOverrides::m_preset)) + ->Enum(OutputDeviceTransformType::NumOutputDeviceTransformTypes)>( + "OutputDeviceTransformType_NumOutputDeviceTransformTypes") + ->Enum(OutputDeviceTransformType::OutputDeviceTransformType_48Nits)>( + "OutputDeviceTransformType_48Nits") + ->Enum(OutputDeviceTransformType::OutputDeviceTransformType_1000Nits)>( + "OutputDeviceTransformType_1000Nits") + ->Enum(OutputDeviceTransformType::OutputDeviceTransformType_2000Nits)>( + "OutputDeviceTransformType_2000Nits") + ->Enum(OutputDeviceTransformType::OutputDeviceTransformType_4000Nits)>( + "OutputDeviceTransformType_4000Nits") ->Property("alterSurround", BehaviorValueProperty(&AcesParameterOverrides::m_alterSurround)) ->Property("applyDesaturation", BehaviorValueProperty(&AcesParameterOverrides::m_applyDesaturation)) ->Property("applyCATD60toD65", BehaviorValueProperty(&AcesParameterOverrides::m_applyCATD60toD65)) diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index cbee40616e..b9b4699ec7 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -304,6 +304,30 @@ namespace AZ } } + void MeshFeatureProcessor::SetLocalAabb(const MeshHandle& meshHandle, const AZ::Aabb& localAabb) + { + if (meshHandle.IsValid()) + { + MeshDataInstance& meshData = *meshHandle; + meshData.m_aabb = localAabb; + meshData.m_cullBoundsNeedsUpdate = true; + meshData.m_objectSrgNeedsUpdate = true; + } + }; + + AZ::Aabb MeshFeatureProcessor::GetLocalAabb(const MeshHandle& meshHandle) const + { + if (meshHandle.IsValid()) + { + return meshHandle->m_aabb; + } + else + { + AZ_Assert(false, "Invalid mesh handle"); + return Aabb::CreateNull(); + } + } + Transform MeshFeatureProcessor::GetTransform(const MeshHandle& meshHandle) { if (meshHandle.IsValid()) @@ -603,6 +627,8 @@ namespace AZ SetRayTracingData(); } + m_aabb = model->GetModelAsset()->GetAabb(); + m_cullableNeedsRebuild = true; m_cullBoundsNeedsUpdate = true; m_objectSrgNeedsUpdate = true; @@ -996,7 +1022,7 @@ namespace AZ RPI::Cullable::CullData& cullData = m_cullable.m_cullData; RPI::Cullable::LodData& lodData = m_cullable.m_lodData; - const Aabb& localAabb = m_model->GetAabb(); + const Aabb& localAabb = m_aabb; lodData.m_lodSelectionRadius = 0.5f*localAabb.GetExtents().GetMaxElement(); const size_t modelLodCount = m_model->GetLodCount(); @@ -1077,7 +1103,7 @@ namespace AZ Vector3 center; float radius; - Aabb localAabb = m_model->GetAabb(); + Aabb localAabb = m_aabb; localAabb.MultiplyByScale(nonUniformScale); localAabb.GetTransformedAabb(localToWorld).GetAsSphere(center, radius); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/LuminanceHistogramGeneratorPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/LuminanceHistogramGeneratorPass.cpp index 55c64c1f33..fb430ac3d3 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/LuminanceHistogramGeneratorPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/LuminanceHistogramGeneratorPass.cpp @@ -67,7 +67,7 @@ namespace AZ desc.m_bufferName = "LuminanceHistogramBuffer"; desc.m_elementSize = sizeof(uint32_t); desc.m_byteCount = NumHistogramBins * sizeof(uint32_t); - desc.m_elementFormat = RHI::Format::R32_UINT; + desc.m_elementFormat = RHI::Format::Unknown; m_histogram = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); AZ_Assert(m_histogram != nullptr, "Unable to allocate buffer"); } diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h index 4a2ccce1d4..be8dc6d596 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h @@ -37,6 +37,9 @@ namespace AZ //! to store the previous frame image Data::Instance& GetFrameBufferImageAttachment() { return m_frameBufferImageAttachment; } + //! Returns the number of mip levels in the blur + uint32_t GetNumBlurMips() const { return m_numBlurMips; } + private: explicit ReflectionScreenSpaceBlurPass(const RPI::PassDescriptor& descriptor); diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.cpp b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.cpp new file mode 100644 index 0000000000..ab09d7175a --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.cpp @@ -0,0 +1,58 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include "ReflectionScreenSpaceCompositePass.h" +#include "ReflectionScreenSpaceBlurPass.h" +#include +#include + +namespace AZ +{ + namespace Render + { + RPI::Ptr ReflectionScreenSpaceCompositePass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew ReflectionScreenSpaceCompositePass(descriptor); + return AZStd::move(pass); + } + + ReflectionScreenSpaceCompositePass::ReflectionScreenSpaceCompositePass(const RPI::PassDescriptor& descriptor) + : RPI::FullscreenTrianglePass(descriptor) + { + } + + void ReflectionScreenSpaceCompositePass::CompileResources([[maybe_unused]] const RHI::FrameGraphCompileContext& context) + { + if (!m_shaderResourceGroup) + { + return; + } + + RPI::PassHierarchyFilter passFilter(AZ::Name("ReflectionScreenSpaceBlurPass")); + const AZStd::vector& passes = RPI::PassSystemInterface::Get()->FindPasses(passFilter); + if (!passes.empty()) + { + Render::ReflectionScreenSpaceBlurPass* blurPass = azrtti_cast(passes.front()); + + // compute the max mip level based on the available mips in the previous frame image, and capping it + // to stay within a range that has reasonable data + const uint32_t MaxNumRoughnessMips = 8; + uint32_t maxMipLevel = AZStd::min(MaxNumRoughnessMips, blurPass->GetNumBlurMips()) - 1; + + auto constantIndex = m_shaderResourceGroup->FindShaderInputConstantIndex(Name("m_maxMipLevel")); + m_shaderResourceGroup->SetConstant(constantIndex, maxMipLevel); + } + + FullscreenTrianglePass::CompileResources(context); + } + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.h b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.h new file mode 100644 index 0000000000..110673541e --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.h @@ -0,0 +1,43 @@ +/* +* 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 + +namespace AZ +{ + namespace Render + { + //! This pass composites the screenspace reflection trace onto the reflection buffer. + class ReflectionScreenSpaceCompositePass + : public RPI::FullscreenTrianglePass + { + AZ_RPI_PASS(ReflectionScreenSpaceCompositePass); + + public: + AZ_RTTI(Render::ReflectionScreenSpaceCompositePass, "{88739CC9-C3F1-413A-A527-9916C697D93A}", FullscreenTrianglePass); + AZ_CLASS_ALLOCATOR(Render::ReflectionScreenSpaceCompositePass, SystemAllocator, 0); + + //! Creates a new pass without a PassTemplate + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + private: + explicit ReflectionScreenSpaceCompositePass(const RPI::PassDescriptor& descriptor); + + // Pass Overrides... + void CompileResources(const RHI::FrameGraphCompileContext& context) override; + }; + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp index 343370ae35..a87d272448 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp @@ -629,6 +629,11 @@ namespace AZ Data::Asset lodAsset; modelLodCreator.End(lodAsset); + if (!lodAsset.IsReady()) + { + // [GFX TODO] During mesh reload the modelLodCreator could report errors and result in the lodAsset not ready. + return nullptr; + } modelCreator.AddLodAsset(AZStd::move(lodAsset)); lodIndex++; diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshOutputStreamManager.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshOutputStreamManager.cpp index 1cb782d8b1..1b14611e84 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshOutputStreamManager.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshOutputStreamManager.cpp @@ -67,8 +67,8 @@ namespace AZ creator.SetBuffer(nullptr, 0, bufferDescriptor); RHI::BufferViewDescriptor viewDescriptor; - viewDescriptor.m_elementFormat = RHI::Format::R32_FLOAT; - viewDescriptor.m_elementSize = RHI::GetFormatSize(viewDescriptor.m_elementFormat); + viewDescriptor.m_elementFormat = RHI::Format::Unknown; + viewDescriptor.m_elementSize = sizeof(float); viewDescriptor.m_elementCount = aznumeric_cast(m_sizeInBytes) / viewDescriptor.m_elementSize; viewDescriptor.m_elementOffset = 0; creator.SetBufferViewDescriptor(viewDescriptor); diff --git a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake index a759de77fa..a656558abf 100644 --- a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake +++ b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake @@ -267,6 +267,8 @@ set(FILES Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurChildPass.cpp Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurChildPass.h + Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.cpp + Source/ReflectionScreenSpace/ReflectionScreenSpaceCompositePass.h Source/ReflectionScreenSpace/ReflectionCopyFrameBufferPass.cpp Source/ReflectionScreenSpace/ReflectionCopyFrameBufferPass.h Source/ScreenSpace/DeferredFogSettings.cpp diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/RenderStates.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/RenderStates.h index 51b15c4bcf..651be829b5 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/RenderStates.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Reflect/RenderStates.h @@ -160,6 +160,16 @@ namespace AZ StencilState m_stencil; }; + enum class WriteChannelMask : uint8_t + { + ColorWriteMaskNone = 0, + ColorWriteMaskRed = AZ_BIT(0), + ColorWriteMaskGreen = AZ_BIT(1), + ColorWriteMaskBlue = AZ_BIT(2), + ColorWriteMaskAlpha = AZ_BIT(3), + ColorWriteMaskAll = ColorWriteMaskRed | ColorWriteMaskGreen | ColorWriteMaskBlue | ColorWriteMaskAlpha + }; + struct TargetBlendState { AZ_TYPE_INFO(TargetBlendState, "{2CDF00FE-614D-44FC-929F-E6B50C348578}"); diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/ShaderResourceGroupData.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/ShaderResourceGroupData.h index ce2c6c77ec..afcab28a1d 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/ShaderResourceGroupData.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/ShaderResourceGroupData.h @@ -410,7 +410,6 @@ namespace AZ // For any other type the buffer view's element size should match the stride. if (shaderInputBuffer.m_strideSize != bufferViewDescriptor.m_elementSize) { - // [GFX TODO][ATOM-5735][AZSL] ByteAddressBuffer shader input is setting a stride of 16 instead of 4 AZ_Error("ShaderResourceGroupData", false, "Buffer Input '%s[%d]': Does not match expected stride size %d", shaderInputBuffer.m_name.GetCStr(), arrayIndex, bufferViewDescriptor.m_elementSize); return false; diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandList.h b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandList.h index a6fe57f6d8..6f09ea1d5f 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandList.h +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandList.h @@ -271,6 +271,12 @@ namespace AZ ShaderResourceBindings& bindings = GetShaderResourceBindingsByPipelineType(pipelineType); const PipelineState* pipelineState = static_cast(item.m_pipelineState); + if(!pipelineState) + { + AZ_Assert(false, "Pipeline state not provided"); + return false; + } + bool updatePipelineState = m_state.m_pipelineState != pipelineState; // The pipeline state gets set first. if (updatePipelineState) diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.cpp index a3d14e3803..a29c7ae402 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.cpp @@ -10,6 +10,7 @@ * */ #include "RHI/Atom_RHI_DX12_precompiled.h" +#include #include #include #include @@ -1268,7 +1269,7 @@ namespace AZ dst.BlendOpAlpha = ConvertBlendOp(src.m_blendAlphaOp); dst.DestBlend = ConvertBlendFactor(src.m_blendDest); dst.DestBlendAlpha = ConvertBlendFactor(src.m_blendAlphaDest); - dst.RenderTargetWriteMask = src.m_writeMask; + dst.RenderTargetWriteMask = ConvertColorWriteMask(src.m_writeMask); dst.SrcBlend = ConvertBlendFactor(src.m_blendSource); dst.SrcBlendAlpha = ConvertBlendFactor(src.m_blendAlphaSource); dst.LogicOp = D3D12_LOGIC_OP_CLEAR; @@ -1355,6 +1356,38 @@ namespace AZ }; return table[(uint32_t)mask]; } + + uint8_t ConvertColorWriteMask(uint8_t writeMask) + { + uint8_t dflags = 0; + if(writeMask == 0) + { + return dflags; + } + + if(RHI::CheckBitsAll(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskAll))) + { + return D3D12_COLOR_WRITE_ENABLE_ALL; + } + + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskRed))) + { + dflags |= D3D12_COLOR_WRITE_ENABLE_RED; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskGreen))) + { + dflags |= D3D12_COLOR_WRITE_ENABLE_GREEN; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskBlue))) + { + dflags |= D3D12_COLOR_WRITE_ENABLE_BLUE; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskAlpha))) + { + dflags |= D3D12_COLOR_WRITE_ENABLE_ALPHA; + } + return dflags; + } D3D12_DEPTH_STENCIL_DESC ConvertDepthStencilState(const RHI::DepthStencilState& depthStencil) { diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.h b/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.h index d8385203f8..640a5fef31 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.h +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/Conversions.h @@ -164,5 +164,7 @@ namespace AZ uint32_t shaderRegisterSpace, D3D12_SHADER_VISIBILITY shaderVisibility, D3D12_STATIC_SAMPLER_DESC& staticSamplerDesc); + + uint8_t ConvertColorWriteMask(uint8_t writeMask); } } diff --git a/Gems/Atom/RHI/Metal/Code/Source/Platform/Mac/RHI/Metal_RHI_Mac.cpp b/Gems/Atom/RHI/Metal/Code/Source/Platform/Mac/RHI/Metal_RHI_Mac.cpp index cc8c4c80c0..b81c51a57b 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/Platform/Mac/RHI/Metal_RHI_Mac.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/Platform/Mac/RHI/Metal_RHI_Mac.cpp @@ -94,7 +94,7 @@ namespace Platform void ResizeInternal(RHIMetalView* metalView, CGSize viewSize) { - [metalView resizeSubviewsWithOldSize:viewSize]; + [metalView.metalLayer setDrawableSize: viewSize]; } RHIMetalView* GetMetalView(NativeWindowType* nativeWindow) diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.cpp index 6ffd15e0bc..c433a6f9cf 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.cpp @@ -386,35 +386,33 @@ namespace AZ void ArgumentBuffer::AddUntrackedResourcesToEncoder(id commandEncoder, const ShaderResourceGroupVisibility& srgResourcesVisInfo) const { + //Map to cache all the resources based on the usage as we can batch all the resources for a given usage + ComputeResourcesToMakeResidentMap resourcesToMakeResidentCompute; + //Map to cache all the resources based on the usage and shader stage as we can batch all the resources for a given usage/shader usage + GraphicsResourcesToMakeResidentMap resourcesToMakeResidentGraphics; + + //Cache the constant buffer associated with a srg if (m_constantBufferSize) { uint8_t numBitsSet = RHI::CountBitsSet(static_cast(srgResourcesVisInfo.m_constantDataStageMask)); if( numBitsSet > 0) { + id mtlconstantBufferResource = m_constantBuffer.GetGpuAddress>(); if(RHI::CheckBitsAny(srgResourcesVisInfo.m_constantDataStageMask, RHI::ShaderStageMask::Compute)) { - [static_cast>(commandEncoder) useResource:m_constantBuffer.GetGpuAddress>() usage:MTLResourceUsageRead]; + resourcesToMakeResidentCompute[MTLResourceUsageRead].emplace(mtlconstantBufferResource); } else { MTLRenderStages mtlRenderStages = GetRenderStages(srgResourcesVisInfo.m_constantDataStageMask); - [static_cast>(commandEncoder) useResource:m_constantBuffer.GetGpuAddress>() - usage:MTLResourceUsageRead - stages:mtlRenderStages]; + AZStd::pair key = AZStd::make_pair(MTLResourceUsageRead, mtlRenderStages); + resourcesToMakeResidentGraphics[key].emplace(mtlconstantBufferResource); } - } } - ApplyUseResource(commandEncoder, m_resourceBindings, srgResourcesVisInfo); - } - - void ArgumentBuffer::ApplyUseResource(id encoder, - const ResourceBindingsMap& resourceMap, - const ShaderResourceGroupVisibility& srgResourcesVisInfo) const - { - - CommandEncoderType encodeType = CommandEncoderType::Invalid; - for (const auto& it : resourceMap) + + //Cach all the resources within a srg that are used by the shader based on the visibility information + for (const auto& it : m_resourceBindings) { //Extract the visibility mask for the give resource auto visMaskIt = srgResourcesVisInfo.m_resourcesStageMask.find(it.first); @@ -426,40 +424,55 @@ namespace AZ { if(RHI::CheckBitsAny(visMaskIt->second, RHI::ShaderStageMask::Compute)) { - //Call UseResource on all resources for Compute stage - ApplyUseResourceToCompute(encoder, it.second); - encodeType = CommandEncoderType::Compute; + CollectResourcesForCompute(commandEncoder, it.second, resourcesToMakeResidentCompute); } else { - //Call UseResource on all resources for Vertex and Fragment stages - AZ_Assert(RHI::CheckBitsAny(visMaskIt->second, RHI::ShaderStageMask::Vertex) || RHI::CheckBitsAny(visMaskIt->second, RHI::ShaderStageMask::Fragment), "The visibility mask %i is not set for Vertex or fragment stage", visMaskIt->second); - ApplyUseResourceToGraphic(encoder, visMaskIt->second, it.second); - encodeType = CommandEncoderType::Render; + bool isBoundToGraphics = RHI::CheckBitsAny(visMaskIt->second, RHI::ShaderStageMask::Vertex) || RHI::CheckBitsAny(visMaskIt->second, RHI::ShaderStageMask::Fragment); + AZ_Assert(isBoundToGraphics, "The visibility mask %i is not set for Vertex or fragment stage", visMaskIt->second); + CollectResourcesForGraphics(commandEncoder, visMaskIt->second, it.second, resourcesToMakeResidentGraphics); } } } + + //Call UseResource on all resources for Compute stage + for (const auto& key : resourcesToMakeResidentCompute) + { + AZStd::vector> resourcesToProcessVec(key.second.begin(), key.second.end()); + [static_cast>(commandEncoder) useResources: &resourcesToProcessVec[0] + count: resourcesToProcessVec.size() + usage: key.first]; + } + + //Call UseResource on all resources for Vertex and Fragment stages + for (const auto& key : resourcesToMakeResidentGraphics) + { + AZStd::vector> resourcesToProcessVec(key.second.begin(), key.second.end()); + [static_cast>(commandEncoder) useResources: &resourcesToProcessVec[0] + count: resourcesToProcessVec.size() + usage: key.first.first + stages: key.first.second]; + } } - - void ArgumentBuffer::ApplyUseResourceToCompute(id encoder, const ResourceBindingsSet& resourceBindingDataSet) const + + void ArgumentBuffer::CollectResourcesForCompute(id encoder, + const ResourceBindingsSet& resourceBindingDataSet, + ComputeResourcesToMakeResidentMap& resourcesToMakeResidentMap) const { for (const auto& resourceBindingData : resourceBindingDataSet) { ResourceType rescType = resourceBindingData.m_resourcPtr->GetResourceType(); + MTLResourceUsage resourceUsage = MTLResourceUsageRead; switch(rescType) { case ResourceType::MtlTextureType: { - MTLResourceUsage resourceUsage = GetImageResourceUsage(resourceBindingData.m_imageAccess); - [static_cast>(encoder) useResource:resourceBindingData.m_resourcPtr->GetGpuAddress>() usage:resourceUsage]; - + resourceUsage |= GetImageResourceUsage(resourceBindingData.m_imageAccess); break; } case ResourceType::MtlBufferType: { - MTLResourceUsage resourceUsage = GetBufferResourceUsage(resourceBindingData.m_bufferAccess); - [static_cast>(encoder) useResource:resourceBindingData.m_resourcPtr->GetGpuAddress>() usage:resourceUsage]; - + resourceUsage |= GetBufferResourceUsage(resourceBindingData.m_bufferAccess); break; } default: @@ -467,13 +480,20 @@ namespace AZ AZ_Assert(false, "Undefined Resource type"); } } + + id mtlResourceToBind = resourceBindingData.m_resourcPtr->GetGpuAddress>(); + resourcesToMakeResidentMap[resourceUsage].emplace(mtlResourceToBind); } } - void ArgumentBuffer::ApplyUseResourceToGraphic(id encoder, RHI::ShaderStageMask visShaderMask, const ResourceBindingsSet& resourceBindingDataSet) const + void ArgumentBuffer::CollectResourcesForGraphics(id encoder, + RHI::ShaderStageMask visShaderMask, + const ResourceBindingsSet& resourceBindingDataSet, + GraphicsResourcesToMakeResidentMap& resourcesToMakeResidentMap) const { - + MTLRenderStages mtlRenderStages = GetRenderStages(visShaderMask); + MTLResourceUsage resourceUsage = MTLResourceUsageRead; for (const auto& resourceBindingData : resourceBindingDataSet) { ResourceType rescType = resourceBindingData.m_resourcPtr->GetResourceType(); @@ -481,20 +501,12 @@ namespace AZ { case ResourceType::MtlTextureType: { - MTLResourceUsage resourceUsage = GetImageResourceUsage(resourceBindingData.m_imageAccess); - [static_cast>(encoder) useResource:resourceBindingData.m_resourcPtr->GetGpuAddress>() - usage:resourceUsage - stages:mtlRenderStages]; - + resourceUsage |= GetImageResourceUsage(resourceBindingData.m_imageAccess); break; } case ResourceType::MtlBufferType: { - MTLResourceUsage resourceUsage = GetBufferResourceUsage(resourceBindingData.m_bufferAccess); - [static_cast>(encoder) useResource:resourceBindingData.m_resourcPtr->GetGpuAddress>() - usage:resourceUsage - stages:mtlRenderStages]; - + resourceUsage |= GetBufferResourceUsage(resourceBindingData.m_bufferAccess); break; } default: @@ -502,8 +514,11 @@ namespace AZ AZ_Assert(false, "Undefined Resource type"); } } + + AZStd::pair key = AZStd::make_pair(resourceUsage, mtlRenderStages); + id mtlResourceToBind = resourceBindingData.m_resourcPtr->GetGpuAddress>(); + resourcesToMakeResidentMap[key].emplace(mtlResourceToBind); } } - } } diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.h b/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.h index a5a8e00e69..29d7d5e239 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.h +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/ArgumentBuffer.h @@ -119,8 +119,19 @@ namespace AZ using ResourceBindingsMap = AZStd::unordered_map; ResourceBindingsMap m_resourceBindings; - void ApplyUseResourceToCompute(id encoder, const ResourceBindingsSet& resourceBindingData) const; - void ApplyUseResourceToGraphic(id encoder, RHI::ShaderStageMask visShaderMask, const ResourceBindingsSet& resourceBindingDataSet) const; + static const int MaxEntriesInArgTable = 31; + //Map to cache all the resources based on the usage as we can batch all the resources for a given usage. + using ComputeResourcesToMakeResidentMap = AZStd::unordered_map>>; + //Map to cache all the resources based on the usage and shader stage as we can batch all the resources for a given usage/shader usage. + using GraphicsResourcesToMakeResidentMap = AZStd::unordered_map, AZStd::unordered_set>>; + + void CollectResourcesForCompute(id encoder, + const ResourceBindingsSet& resourceBindingData, + ComputeResourcesToMakeResidentMap& resourcesToMakeResidentMap) const; + void CollectResourcesForGraphics(id encoder, + RHI::ShaderStageMask visShaderMask, + const ResourceBindingsSet& resourceBindingDataSet, + GraphicsResourcesToMakeResidentMap& resourcesToMakeResidentMap) const; //! Use visibility information to call UseResource on all resources for this Argument Buffer void ApplyUseResource(id encoder, const ResourceBindingsMap& resourceMap, @@ -144,8 +155,6 @@ namespace AZ #endif ShaderResourceGroupPool* m_srgPool = nullptr; - - static const int MaxEntriesInArgTable = 31; NSCache* m_samplerCache; }; } diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/AsyncUploadQueue.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/AsyncUploadQueue.cpp index 594a4931b9..29e0b19b75 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/AsyncUploadQueue.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/AsyncUploadQueue.cpp @@ -85,16 +85,36 @@ namespace AZ uint64_t AsyncUploadQueue::QueueUpload(const RHI::BufferStreamRequest& uploadRequest) { - uint64_t queueValue = m_uploadFence.Increment(); + Buffer& destBuffer = static_cast(*uploadRequest.m_buffer); + const MemoryView& destMemoryView = destBuffer.GetMemoryView(); + MTLStorageMode mtlStorageMode = destBuffer.GetMemoryView().GetStorageMode(); + RHI::BufferPool& bufferPool = static_cast(*destBuffer.GetPool()); + + // No need to use staging buffers since it's host memory. + // We just map, copy and then unmap. + if(mtlStorageMode == MTLStorageModeShared || mtlStorageMode == GetCPUGPUMemoryMode()) + { + RHI::BufferMapRequest mapRequest; + mapRequest.m_buffer = uploadRequest.m_buffer; + mapRequest.m_byteCount = uploadRequest.m_byteCount; + mapRequest.m_byteOffset = uploadRequest.m_byteOffset; + RHI::BufferMapResponse mapResponse; + bufferPool.MapBuffer(mapRequest, mapResponse); + ::memcpy(mapResponse.m_data, uploadRequest.m_sourceData, uploadRequest.m_byteCount); + bufferPool.UnmapBuffer(*uploadRequest.m_buffer); + if (uploadRequest.m_fenceToSignal) + { + uploadRequest.m_fenceToSignal->SignalOnCpu(); + } + return m_uploadFence.GetPendingValue(); + } - const MemoryView& memoryView = static_cast(*uploadRequest.m_buffer).GetMemoryView(); - RHI::Ptr buffer = memoryView.GetMemory(); - Fence* fenceToSignal = nullptr; uint64_t fenceToSignalValue = 0; - size_t byteCount = uploadRequest.m_byteCount; - size_t byteOffset = memoryView.GetOffset() + uploadRequest.m_byteOffset; + size_t byteOffset = destMemoryView.GetOffset() + uploadRequest.m_byteOffset; + uint64_t queueValue = m_uploadFence.Increment(); + const uint8_t* sourceData = reinterpret_cast(uploadRequest.m_sourceData); if (uploadRequest.m_fenceToSignal) @@ -125,11 +145,11 @@ namespace AZ } id blitEncoder = [framePacket->m_mtlCommandBuffer blitCommandEncoder]; - [blitEncoder copyFromBuffer:framePacket->m_stagingResource - sourceOffset:0 - toBuffer:buffer->GetGpuAddress>() - destinationOffset:byteOffset + pendingByteOffset - size:bytesToCopy]; + [blitEncoder copyFromBuffer: framePacket->m_stagingResource + sourceOffset: 0 + toBuffer: destMemoryView.GetGpuAddress>() + destinationOffset: byteOffset + pendingByteOffset + size: bytesToCopy]; [blitEncoder endEncoding]; blitEncoder = nil; diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.cpp index 2ca8303dc9..b986b8ea75 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.cpp @@ -40,9 +40,8 @@ namespace AZ buffer->m_pendingResolves++; uploadRequest.m_attachmentBuffer = buffer; - uploadRequest.m_byteOffset = request.m_byteOffset; + uploadRequest.m_byteOffset = buffer->GetMemoryView().GetOffset() + request.m_byteOffset; uploadRequest.m_stagingBuffer = stagingBuffer; - uploadRequest.m_byteSize = request.m_byteCount; return stagingBuffer->GetMemoryView().GetCpuAddress(); } @@ -64,12 +63,15 @@ namespace AZ AZ_Assert(stagingBuffer, "Staging Buffer is null."); AZ_Assert(destBuffer, "Attachment Buffer is null."); + //Inform the GPU that the CPU has modified the staging buffer. + Platform::SynchronizeBufferOnCPU(stagingBuffer->GetMemoryView().GetGpuAddress>(), stagingBuffer->GetMemoryView().GetOffset(), stagingBuffer->GetMemoryView().GetSize()); + RHI::CopyBufferDescriptor copyDescriptor; copyDescriptor.m_sourceBuffer = stagingBuffer; - copyDescriptor.m_sourceOffset = 0; + copyDescriptor.m_sourceOffset = stagingBuffer->GetMemoryView().GetOffset(); copyDescriptor.m_destinationBuffer = destBuffer; copyDescriptor.m_destinationOffset = static_cast(packet.m_byteOffset); - copyDescriptor.m_size = static_cast(packet.m_byteSize); + copyDescriptor.m_size = stagingBuffer->GetMemoryView().GetSize(); commandList.Submit(RHI::CopyItem(copyDescriptor)); device.QueueForRelease(stagingBuffer->GetMemoryView()); diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.h b/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.h index c62d9494db..3e60bb31fa 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.h +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/BufferPoolResolver.h @@ -54,7 +54,6 @@ namespace AZ Buffer* m_attachmentBuffer = nullptr; RHI::Ptr m_stagingBuffer; size_t m_byteOffset = 0; - size_t m_byteSize = 0; }; AZStd::mutex m_uploadPacketsLock; diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.cpp index 3eca8dabd8..7f65c9ea47 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -249,66 +250,191 @@ namespace AZ ShaderResourceBindings& bindings = GetShaderResourceBindingsByPipelineType(stateType); const PipelineLayout& pipelineLayout = pipelineState->GetPipelineLayout(); - for (uint32_t srgIndex = 0; srgIndex < RHI::Limits::Pipeline::ShaderResourceGroupCountMax; ++srgIndex) + uint32_t bufferVertexRegisterIdMin = RHI::Limits::Pipeline::ShaderResourceGroupCountMax; + uint32_t bufferFragmentOrComputeRegisterIdMin = RHI::Limits::Pipeline::ShaderResourceGroupCountMax; + uint32_t bufferVertexRegisterIdMax = 0; + uint32_t bufferFragmentOrComputeRegisterIdMax = 0; + + //Arrays to cache all the buffers and offsets in order to make batch calls + MetalArgumentBufferArray mtlVertexArgBuffers; + MetalArgumentBufferArrayOffsets mtlVertexArgBufferOffsets; + MetalArgumentBufferArray mtlFragmentOrComputeArgBuffers; + MetalArgumentBufferArrayOffsets mtlFragmentOrComputeArgBufferOffsets; + + mtlVertexArgBuffers.fill(nil); + mtlFragmentOrComputeArgBuffers.fill(nil); + mtlVertexArgBufferOffsets.fill(0); + mtlFragmentOrComputeArgBufferOffsets.fill(0); + + for (uint32_t slot = 0; slot < RHI::Limits::Pipeline::ShaderResourceGroupCountMax; ++slot) { - const ShaderResourceGroup* shaderResourceGroup = bindings.m_srgsBySlot[srgIndex]; - uint32_t slotIndex = pipelineLayout.GetSlotByIndex(srgIndex); + const ShaderResourceGroup* shaderResourceGroup = bindings.m_srgsBySlot[slot]; + uint32_t slotIndex = pipelineLayout.GetIndexBySlot(slot); if(!shaderResourceGroup || slotIndex == RHI::Limits::Pipeline::ShaderResourceGroupCountMax) { continue; } - if (bindings.m_srgsByIndex[srgIndex] != shaderResourceGroup) + uint32_t srgVisIndex = pipelineLayout.GetIndexBySlot(shaderResourceGroup->GetBindingSlot()); + const RHI::ShaderStageMask& srgVisInfo = pipelineLayout.GetSrgVisibility(srgVisIndex); + + bool isSrgUpdatd = bindings.m_srgsByIndex[slot] != shaderResourceGroup; + if(isSrgUpdatd) { - bindings.m_srgsByIndex[srgIndex] = shaderResourceGroup; + bindings.m_srgsByIndex[slot] = shaderResourceGroup; auto& compiledArgBuffer = shaderResourceGroup->GetCompiledArgumentBuffer(); - id argBuffer = compiledArgBuffer.GetArgEncoderBuffer(); size_t argBufferOffset = compiledArgBuffer.GetOffset(); - - uint32_t srgVisIndex = pipelineLayout.GetSlotByIndex(shaderResourceGroup->GetBindingSlot()); - const RHI::ShaderStageMask& srgVisInfo = pipelineLayout.GetSrgVisibility(srgVisIndex); - + if(srgVisInfo != RHI::ShaderStageMask::None) { - const ShaderResourceGroupVisibility& srgResourcesVisInfo = pipelineLayout.GetSrgResourcesVisibility(srgVisIndex); - - //For graphics and compute encoder bind the argument buffer and - //make the resource resident for the duration of the work associated with the current scope - //and ensure that it's in a format compatible with the appropriate metal function. + //For graphics and compute shader stages, cache all the argument buffers, offsets and track the min/max indices if(m_commandEncoderType == CommandEncoderType::Render) { id renderEncoder = GetEncoder>(); uint8_t numBitsSet = RHI::CountBitsSet(static_cast(srgVisInfo)); if( numBitsSet > 1 || srgVisInfo == RHI::ShaderStageMask::Vertex) { - [renderEncoder setVertexBuffer:argBuffer - offset:argBufferOffset - atIndex:slotIndex]; + mtlVertexArgBuffers[slotIndex] = argBuffer; + mtlVertexArgBufferOffsets[slotIndex] = argBufferOffset; + bufferVertexRegisterIdMin = AZStd::min(slotIndex, bufferVertexRegisterIdMin); + bufferVertexRegisterIdMax = AZStd::max(slotIndex, bufferVertexRegisterIdMax); } if( numBitsSet > 1 || srgVisInfo == RHI::ShaderStageMask::Fragment) { - [renderEncoder setFragmentBuffer:argBuffer - offset:argBufferOffset - atIndex:slotIndex]; + mtlFragmentOrComputeArgBuffers[slotIndex] = argBuffer; + mtlFragmentOrComputeArgBufferOffsets[slotIndex] = argBufferOffset; + bufferFragmentOrComputeRegisterIdMin = AZStd::min(slotIndex, bufferFragmentOrComputeRegisterIdMin); + bufferFragmentOrComputeRegisterIdMax = AZStd::max(slotIndex, bufferFragmentOrComputeRegisterIdMax); } + } + else if(m_commandEncoderType == CommandEncoderType::Compute) + { + mtlFragmentOrComputeArgBuffers[slotIndex] = argBuffer; + mtlFragmentOrComputeArgBufferOffsets[slotIndex] = argBufferOffset; + bufferFragmentOrComputeRegisterIdMin = AZStd::min(slotIndex, bufferFragmentOrComputeRegisterIdMin); + bufferFragmentOrComputeRegisterIdMax = AZStd::max(slotIndex, bufferFragmentOrComputeRegisterIdMax); + } + } + } + + //Check if the srg has been updated or if the srg resources visibility hash has been updated + //as it is possible for draw items to have different PSOs in the same pass. + const AZ::HashValue64 srgResourcesVisHash = pipelineLayout.GetSrgResourcesVisibilityHash(srgVisIndex); + if(bindings.m_srgVisHashByIndex[slot] != srgResourcesVisHash || isSrgUpdatd) + { + bindings.m_srgVisHashByIndex[slot] = srgResourcesVisHash; + if(srgVisInfo != RHI::ShaderStageMask::None) + { + const ShaderResourceGroupVisibility& srgResourcesVisInfo = pipelineLayout.GetSrgResourcesVisibility(srgVisIndex); + + //For graphics and compute encoder make the resource resident (call UseResource) for the duration + //of the work associated with the current scope and ensure that it's in a + //format compatible with the appropriate metal function. + if(m_commandEncoderType == CommandEncoderType::Render) + { shaderResourceGroup->AddUntrackedResourcesToEncoder(m_encoder, srgResourcesVisInfo); } else if(m_commandEncoderType == CommandEncoderType::Compute) { - id computeEncoder = GetEncoder>(); - [computeEncoder setBuffer:argBuffer - offset:argBufferOffset - atIndex:pipelineLayout.GetSlotByIndex(srgIndex)]; shaderResourceGroup->AddUntrackedResourcesToEncoder(m_encoder, srgResourcesVisInfo); } } } } - + + //For graphics and compute encoder bind all the argument buffers + if(m_commandEncoderType == CommandEncoderType::Render) + { + BindArgumentBuffers(RHI::ShaderStage::Vertex, + bufferVertexRegisterIdMin, + bufferVertexRegisterIdMax, + mtlVertexArgBuffers, + mtlVertexArgBufferOffsets); + + BindArgumentBuffers(RHI::ShaderStage::Fragment, + bufferFragmentOrComputeRegisterIdMin, + bufferFragmentOrComputeRegisterIdMax, + mtlFragmentOrComputeArgBuffers, + mtlFragmentOrComputeArgBufferOffsets); + } + else if(m_commandEncoderType == CommandEncoderType::Compute) + { + BindArgumentBuffers(RHI::ShaderStage::Compute, + bufferFragmentOrComputeRegisterIdMin, + bufferFragmentOrComputeRegisterIdMax, + mtlFragmentOrComputeArgBuffers, + mtlFragmentOrComputeArgBufferOffsets); + } + return true; } + + void CommandList::BindArgumentBuffers(RHI::ShaderStage shaderStage, + uint16_t registerIdMin, + uint16_t registerIdMax, + MetalArgumentBufferArray& mtlArgBuffers, + MetalArgumentBufferArrayOffsets mtlArgBufferOffsets) + { + //Metal Api only lets you bind multiple argument buffers in an array as long as there are no gaps in the array + //In order to accomodate that we break up the calls when a gap is noticed in the array and reconfigure the NSRange. + uint16_t startingIndex = registerIdMin; + bool trackingRange = true; + for(int i = registerIdMin; i <= registerIdMax+1; i++) + { + if(trackingRange) + { + if(mtlArgBuffers[i] == nil) + { + NSRange range = { startingIndex, i-startingIndex }; + + switch(shaderStage) + { + case RHI::ShaderStage::Vertex: + { + id renderEncoder = GetEncoder>(); + [renderEncoder setVertexBuffers:&mtlArgBuffers[startingIndex] + offsets:&mtlArgBufferOffsets[startingIndex] + withRange:range]; + break; + } + case RHI::ShaderStage::Fragment: + { + id renderEncoder = GetEncoder>(); + [renderEncoder setFragmentBuffers:&mtlArgBuffers[startingIndex] + offsets:&mtlArgBufferOffsets[startingIndex] + withRange:range]; + break; + } + case RHI::ShaderStage::Compute: + { + id computeEncoder = GetEncoder>(); + [computeEncoder setBuffers:&mtlArgBuffers[startingIndex] + offsets:&mtlArgBufferOffsets[startingIndex] + withRange:range]; + break; + } + default: + { + AZ_Assert(false, "Not supported"); + } + } + + trackingRange = false; + + } + } + else + { + if(mtlArgBuffers[i] != nil) + { + startingIndex = i; + trackingRange = true; + } + } + } + } void CommandList::Submit(const RHI::DrawItem& drawItem) { @@ -447,6 +573,7 @@ namespace AZ for (size_t i = 0; i < bindings.m_srgsByIndex.size(); ++i) { bindings.m_srgsByIndex[i] = nullptr; + bindings.m_srgVisHashByIndex[i] = AZ::HashValue64{0}; } const PipelineLayout& pipelineLayout = pipelineState->GetPipelineLayout(); @@ -469,6 +596,10 @@ namespace AZ void CommandList::SetStreamBuffers(const RHI::StreamBufferView* streams, uint32_t count) { + uint16_t bufferArrayLen = 0; + AZStd::array, METAL_MAX_ENTRIES_BUFFER_ARG_TABLE> mtlStreamBuffers; + AZStd::array mtlStreamBufferOffsets; + AZ::HashValue64 streamsHash = AZ::HashValue64{0}; for (uint32_t i = 0; i < count; ++i) { @@ -479,18 +610,25 @@ namespace AZ { m_state.m_streamsHash = streamsHash; AZ_Assert(count <= METAL_MAX_ENTRIES_BUFFER_ARG_TABLE , "Slots needed cannot exceed METAL_MAX_ENTRIES_BUFFER_ARG_TABLE"); - for (uint32_t i = 0; i < count; ++i) + + NSRange range = {METAL_MAX_ENTRIES_BUFFER_ARG_TABLE - count, count}; + //The stream buffers are populated from bottom to top as the top slots are taken by argument buffers + for (int i = count-1; i >= 0; --i) { if (streams[i].GetBuffer()) { const Buffer * buff = static_cast(streams[i].GetBuffer()); id mtlBuff = buff->GetMemoryView().GetGpuAddress>(); - uint32_t VBIndex = (METAL_MAX_ENTRIES_BUFFER_ARG_TABLE - 1) - i; uint32_t offset = streams[i].GetByteOffset() + buff->GetMemoryView().GetOffset(); - id renderEncoder = GetEncoder>(); - [renderEncoder setVertexBuffer: mtlBuff offset: offset atIndex: VBIndex]; + mtlStreamBuffers[bufferArrayLen] = mtlBuff; + mtlStreamBufferOffsets[bufferArrayLen] = offset; + bufferArrayLen++; } } + id renderEncoder = GetEncoder>(); + [renderEncoder setVertexBuffers: mtlStreamBuffers.data() + offsets: mtlStreamBufferOffsets.data() + withRange: range]; } } diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.h b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.h index 6660beb2c4..8674124cc2 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.h +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandList.h @@ -99,8 +99,17 @@ namespace AZ { AZStd::array m_srgsByIndex; AZStd::array m_srgsBySlot; + AZStd::array m_srgVisHashByIndex; }; + using MetalArgumentBufferArray = AZStd::array, RHI::Limits::Pipeline::ShaderResourceGroupCountMax>; + using MetalArgumentBufferArrayOffsets = AZStd::array; + void BindArgumentBuffers(RHI::ShaderStage shaderStage, + uint16_t registerIdMin, + uint16_t registerIdMax, + MetalArgumentBufferArray& mtlArgBuffers, + MetalArgumentBufferArrayOffsets mtlArgBufferOffsets); + ShaderResourceBindings& GetShaderResourceBindingsByPipelineType(RHI::PipelineStateType pipelineType); //! This is kept as a separate struct so that we can robustly reset it. Every property diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/Conversions.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/Conversions.cpp index f95e871d33..81e589e62e 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/Conversions.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/Conversions.cpp @@ -12,6 +12,7 @@ #include "Atom_RHI_Metal_precompiled.h" #include +#include #include #include #include @@ -456,8 +457,35 @@ namespace AZ MTLColorWriteMask ConvertColorWriteMask(AZ::u8 writeMask) { - //todo::Based on the mask set the correct writemask - return MTLColorWriteMaskAll; + MTLColorWriteMask colorMask = MTLColorWriteMaskNone; + if(writeMask == 0) + { + return colorMask; + } + + if(RHI::CheckBitsAll(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskAll))) + { + return MTLColorWriteMaskAll; + } + + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskRed))) + { + colorMask |= MTLColorWriteMaskRed; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskGreen))) + { + colorMask |= MTLColorWriteMaskGreen; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskBlue))) + { + colorMask |= MTLColorWriteMaskBlue; + } + if (RHI::CheckBitsAny(writeMask, static_cast(RHI::WriteChannelMask::ColorWriteMaskAlpha))) + { + colorMask |= MTLColorWriteMaskAlpha; + } + + return colorMask; } MTLVertexFormat ConvertVertexFormat(RHI::Format format) diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/MemoryPageAllocator.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/MemoryPageAllocator.cpp index 60581aeb2c..88aa4b4ed1 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/MemoryPageAllocator.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/MemoryPageAllocator.cpp @@ -44,7 +44,7 @@ namespace AZ if (memoryView.IsValid()) { heapMemoryUsage.m_residentInBytes += m_descriptor.m_pageSizeInBytes; - memoryView.SetName("BufferPage"); + memoryView.SetName(AZStd::string::format("BufferPage_%s", AZ::Uuid::CreateRandom().ToString().c_str())); } else { diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.cpp index f237884aab..9a30a04deb 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.cpp @@ -70,6 +70,7 @@ namespace AZ m_srgVisibilities.resize(RHI::Limits::Pipeline::ShaderResourceGroupCountMax); m_srgResourcesVisibility.resize(RHI::Limits::Pipeline::ShaderResourceGroupCountMax); + m_srgResourcesVisibilityHash.resize(RHI::Limits::Pipeline::ShaderResourceGroupCountMax); for (uint32_t srgLayoutIdx = 0; srgLayoutIdx < groupLayoutCount; ++srgLayoutIdx) { const RHI::ShaderResourceGroupLayout& srgLayout = *descriptor.GetShaderResourceGroupLayout(srgLayoutIdx); @@ -111,6 +112,7 @@ namespace AZ m_srgVisibilities[srgIndex] = mask; m_srgResourcesVisibility[srgIndex] = srgVis; + m_srgResourcesVisibilityHash[srgIndex] = srgVis.GetHash(); } // Cache the inline constant size and slot index @@ -123,12 +125,12 @@ namespace AZ size_t PipelineLayout::GetSlotByIndex(size_t index) const { - return m_slotToIndexTable[index]; + return m_indexToSlotTable[index]; } size_t PipelineLayout::GetIndexBySlot(size_t slot) const { - return m_indexToSlotTable[slot]; + return m_slotToIndexTable[slot]; } const RHI::ShaderStageMask& PipelineLayout::GetSrgVisibility(uint32_t index) const @@ -141,6 +143,11 @@ namespace AZ return m_srgResourcesVisibility[index]; } + const AZ::HashValue64 PipelineLayout::GetSrgResourcesVisibilityHash(uint32_t index) const + { + return m_srgResourcesVisibilityHash[index]; + } + uint32_t PipelineLayout::GetRootConstantsSize() const { return m_rootConstantsSize; diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.h b/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.h index e8cd249393..6fd06ea29b 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.h +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/PipelineLayout.h @@ -57,6 +57,9 @@ namespace AZ /// Returns srgVisibility data const ShaderResourceGroupVisibility& GetSrgResourcesVisibility(uint32_t index) const; + /// Returns srgVisibility hash + const AZ::HashValue64 GetSrgResourcesVisibilityHash(uint32_t index) const; + /// Returns the root constant specific layout information uint32_t GetRootConstantsSize() const; uint32_t GetRootConstantsSlotIndex() const; @@ -84,6 +87,9 @@ namespace AZ /// Cache Visibility across all the resources within the SRG AZStd::fixed_vector m_srgResourcesVisibility; + /// Cache Visibility hash across all the resources within the SRG + AZStd::fixed_vector m_srgResourcesVisibilityHash; + uint32_t m_rootConstantSlotIndex = (uint32_t)-1; uint32_t m_rootConstantsSize = 0; }; diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp index 1f870548cc..94599bc390 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp @@ -73,6 +73,10 @@ namespace AZ m_metalView.metalLayer.drawableSize = CGSizeMake(descriptor.m_dimensions.m_imageWidth, descriptor.m_dimensions.m_imageHeight); } + else + { + AddSubView(); + } m_drawables.resize(descriptor.m_dimensions.m_imageCount); @@ -83,6 +87,20 @@ namespace AZ return RHI::ResultCode::Success; } + void SwapChain::AddSubView() + { + NativeViewType* superView = reinterpret_cast(m_nativeWindow); + + CGFloat screenScale = Platform::GetScreenScale(); + CGRect screenBounds = [superView bounds]; + m_metalView = [[RHIMetalView alloc] initWithFrame: screenBounds + scale: screenScale + device: m_mtlDevice]; + + [m_metalView retain]; + [superView addSubview: m_metalView]; + } + void SwapChain::ShutdownInternal() { if (m_viewController) @@ -161,16 +179,7 @@ namespace AZ } else { - NativeViewType* superView = reinterpret_cast(m_nativeWindow); - - CGFloat screenScale = Platform::GetScreenScale(); - CGRect screenBounds = [superView bounds]; - m_metalView = [[RHIMetalView alloc] initWithFrame: screenBounds - scale: screenScale - device: m_mtlDevice]; - - [m_metalView retain]; - [superView addSubview: m_metalView]; + AddSubView(); } } return RHI::ResultCode::Success; diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.h b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.h index d455f3dd0c..dfe8b3365d 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.h +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.h @@ -49,6 +49,8 @@ namespace AZ RHI::ResultCode ResizeInternal(const RHI::SwapChainDimensions& dimensions, RHI::SwapChainDimensions* nativeDimensions) override; ////////////////////////////////////////////////////////////////////////// + void AddSubView(); + id m_mtlCommandBuffer; RHIMetalView* m_metalView = nullptr; NativeViewControllerType* m_viewController = nullptr; diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Conversion.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Conversion.cpp index accc18b5ec..8cad9b1f56 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Conversion.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/Conversion.cpp @@ -334,19 +334,30 @@ namespace AZ VkColorComponentFlags ConvertComponentFlags(uint8_t sflags) { VkColorComponentFlags dflags = 0; - if (RHI::CheckBitsAny(sflags, static_cast(1))) + + if(sflags == 0) + { + return dflags; + } + + if(RHI::CheckBitsAll(sflags, static_cast(RHI::WriteChannelMask::ColorWriteMaskAll))) + { + return VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + } + + if (RHI::CheckBitsAny(sflags, static_cast(RHI::WriteChannelMask::ColorWriteMaskRed))) { dflags |= VK_COLOR_COMPONENT_R_BIT; } - if (RHI::CheckBitsAny(sflags, static_cast(2))) + if (RHI::CheckBitsAny(sflags, static_cast(RHI::WriteChannelMask::ColorWriteMaskGreen))) { dflags |= VK_COLOR_COMPONENT_G_BIT; } - if (RHI::CheckBitsAny(sflags, static_cast(4))) + if (RHI::CheckBitsAny(sflags, static_cast(RHI::WriteChannelMask::ColorWriteMaskBlue))) { dflags |= VK_COLOR_COMPONENT_B_BIT; } - if (RHI::CheckBitsAny(sflags, static_cast(8))) + if (RHI::CheckBitsAny(sflags, static_cast(RHI::WriteChannelMask::ColorWriteMaskAlpha))) { dflags |= VK_COLOR_COMPONENT_A_BIT; } diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/ImageView.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/ImageView.cpp index 605f61fc33..ec58b5c940 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/ImageView.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/ImageView.cpp @@ -55,7 +55,11 @@ namespace AZ const auto& image = static_cast(resourceBase); const RHI::ImageViewDescriptor& descriptor = GetDescriptor(); - AZ_Assert(image.GetNativeImage() != VK_NULL_HANDLE, "Image has not been initialized."); + // this can happen when image has been invalidated/released right before re-compiling the image + if (image.GetNativeImage() == VK_NULL_HANDLE) + { + return RHI::ResultCode::Fail; + } RHI::Format viewFormat = descriptor.m_overrideFormat; // If an image is not owner of native image, it is a swapchain image. diff --git a/Gems/Atom/RHI/Vulkan/External/glad/2.0.0-beta/include/glad/vulkan.h b/Gems/Atom/RHI/Vulkan/External/glad/2.0.0-beta/include/glad/vulkan.h index a6ee9e8634..33d4b64082 100644 --- a/Gems/Atom/RHI/Vulkan/External/glad/2.0.0-beta/include/glad/vulkan.h +++ b/Gems/Atom/RHI/Vulkan/External/glad/2.0.0-beta/include/glad/vulkan.h @@ -12691,8 +12691,7 @@ static int glad_vk_find_extensions_vulkan( VkPhysicalDevice physical_device) { #endif GLAD_VK_KHR_push_descriptor = glad_vk_has_extension("VK_KHR_push_descriptor", extension_count, extensions); GLAD_VK_KHR_ray_tracing = (glad_vk_has_extension("VK_KHR_acceleration_structure", extension_count, extensions) - && glad_vk_has_extension("VK_KHR_ray_tracing_pipeline", extension_count, extensions) - && glad_vk_has_extension("VK_KHR_ray_query", extension_count, extensions)); + && glad_vk_has_extension("VK_KHR_ray_tracing_pipeline", extension_count, extensions)); GLAD_VK_KHR_relaxed_block_layout = glad_vk_has_extension("VK_KHR_relaxed_block_layout", extension_count, extensions); GLAD_VK_KHR_sampler_mirror_clamp_to_edge = glad_vk_has_extension("VK_KHR_sampler_mirror_clamp_to_edge", extension_count, extensions); GLAD_VK_KHR_sampler_ycbcr_conversion = glad_vk_has_extension("VK_KHR_sampler_ycbcr_conversion", extension_count, extensions); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h index d9afc54809..846e40ae02 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/DynamicDraw/DynamicDrawContext.h @@ -138,6 +138,12 @@ namespace AZ //! Without per draw viewport, the viewport setup in pass is usually used. void UnsetViewport(); + //! Set stencil reference for following draws which are added to this DynamicDrawContext + void SetStencilReference(uint8_t stencilRef); + + //! Get the current stencil reference. + uint8_t GetStencilReference() const; + //! Draw Indexed primitives with vertex and index data and per draw srg //! The per draw srg need to be provided if it's required by shader. void DrawIndexed(void* vertexData, uint32_t vertexCount, void* indexData, uint32_t indexCount, RHI::IndexFormat indexFormat, Data::Instance < ShaderResourceGroup> drawSrg = nullptr); @@ -204,10 +210,13 @@ namespace AZ bool m_useScissor = false; RHI::Scissor m_scissor; - // current scissor + // current viewport bool m_useViewport = false; RHI::Viewport m_viewport; + // Current stencil reference value + uint8_t m_stencilRef = 0; + // Cached RHI pipeline states for different combination of render states AZStd::unordered_map m_cachedRhiPipelineStates; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h index 514e3e37a5..b403a86f00 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Model/Model.h @@ -32,6 +32,7 @@ namespace AZ : public Data::InstanceData { friend class ModelSystem; + public: AZ_INSTANCE_DATA(Model, "{C30F5522-B381-4B38-BBAF-6E0B1885C8B9}"); AZ_CLASS_ALLOCATOR(Model, AZ::SystemAllocator, 0); @@ -53,8 +54,6 @@ namespace AZ //! Returns whether a buffer upload is pending. bool IsUploadPending() const; - const AZ::Aabb& GetAabb() const; - const Data::Asset& GetModelAsset() const; //! Checks a ray for intersection against this model. The ray must be in the same coordinate space as the model. @@ -105,8 +104,6 @@ namespace AZ // Tracks whether buffers have all been streamed up to the GPU. bool m_isUploadPending = false; - - AZ::Aabb m_aabb; }; } // namespace RPI } // namespace AZ 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 9d0b36d126..a4a26c3070 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 @@ -265,6 +265,12 @@ namespace AZ // Update all bindings on this pass that are connected to bindings on other passes void UpdateConnectedBindings(); + // Update input and input/output bindings on this pass that are connected to bindings on other passes + void UpdateConnectedInputBindings(); + + // Update output bindings on this pass that are connected to bindings on other passes + void UpdateConnectedOutputBindings(); + protected: explicit Pass(const PassDescriptor& descriptor); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h index dfda93dca3..8220d25eb8 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h @@ -57,6 +57,20 @@ namespace AZ } #endif } + + //! Prints a generic message at the appropriate indent level. + template + static void Printf([[maybe_unused]] const char* format, [[maybe_unused]] Args... args) + { +#ifdef AZ_ENABLE_SHADER_RELOAD_DEBUG_TRACKER + if (IsEnabled()) + { + const AZStd::string message = AZStd::string::format(format, args...); + + AZ_TracePrintf("ShaderReloadDebug", "%*s %s \n", s_indent, "", message.c_str()); + } +#endif + } //! Use this utility to call BeginSection(), and automatically call EndSection() when the object goes out of scope. class ScopedSection final diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/DynamicDraw/DynamicDrawContext.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/DynamicDraw/DynamicDrawContext.cpp index 1a575c9385..2b6eb1f690 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/DynamicDraw/DynamicDrawContext.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/DynamicDraw/DynamicDrawContext.cpp @@ -382,6 +382,16 @@ namespace AZ m_useViewport = false; } + void DynamicDrawContext::SetStencilReference(uint8_t stencilRef) + { + m_stencilRef = stencilRef; + } + + uint8_t DynamicDrawContext::GetStencilReference() const + { + return m_stencilRef; + } + void DynamicDrawContext::SetShaderVariant(ShaderVariantId shaderVariantId) { AZ_Assert( m_initialized && m_supportShaderVariants, "DynamicDrawContext is not initialized or unable to support shader variants. " @@ -475,6 +485,9 @@ namespace AZ drawItem.m_viewports = &m_viewport; } + // Set stencil reference. Used when stencil is enabled. + drawItem.m_stencilRef = m_stencilRef; + drawItemInfo.m_sortKey = m_sortKey++; m_cachedDrawItems.emplace_back(drawItemInfo); } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Material/Material.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Material/Material.cpp index 431cb62684..2cff465b95 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Material/Material.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Material/Material.cpp @@ -97,6 +97,7 @@ namespace AZ ShaderReloadNotificationBus::MultiHandler::BusDisconnect(); for (auto& shaderItem : m_shaderCollection) { + ShaderReloadDebugTracker::Printf("(Material has ShaderAsset %p)", shaderItem.GetShaderAsset().Get()); ShaderReloadNotificationBus::MultiHandler::BusConnect(shaderItem.GetShaderAsset().GetId()); } @@ -226,7 +227,7 @@ namespace AZ // AssetBus overrides... void Material::OnAssetReloaded(Data::Asset asset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("Material::OnAssetReloaded %s", asset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Material::OnAssetReloaded %s", this, asset.GetHint().c_str()); Data::Asset newMaterialAsset = { asset.GetAs(), AZ::Data::AssetLoadBehavior::PreLoad }; @@ -241,7 +242,7 @@ namespace AZ // MaterialReloadNotificationBus overrides... void Material::OnMaterialAssetReinitialized(const Data::Asset& materialAsset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("Material::OnMaterialAssetReinitialized %s", materialAsset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Material::OnMaterialAssetReinitialized %s", this, materialAsset.GetHint().c_str()); OnAssetReloaded(materialAsset); } @@ -249,7 +250,7 @@ namespace AZ // ShaderReloadNotificationBus overrides... void Material::OnShaderReinitialized([[maybe_unused]] const Shader& shader) { - ShaderReloadDebugTracker::ScopedSection reloadSection("Material::OnShaderReinitialized %s", shader.GetAsset().GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Material::OnShaderReinitialized %s", this, shader.GetAsset().GetHint().c_str()); // Note that it might not be strictly necessary to reinitialize the entire material, we might be able to get away with // just bumping the m_currentChangeId or some other minor updates. But it's pretty hard to know what exactly needs to be // updated to correctly handle the reload, so it's safer to just reinitialize the whole material. @@ -260,7 +261,7 @@ namespace AZ { // TODO: I think we should make Shader handle OnShaderAssetReinitialized and treat it just like the shader reloaded. - ShaderReloadDebugTracker::ScopedSection reloadSection("Material::OnShaderAssetReinitialized %s", shaderAsset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Material::OnShaderAssetReinitialized %s", this, shaderAsset.GetHint().c_str()); // Note that it might not be strictly necessary to reinitialize the entire material, we might be able to get away with // just bumping the m_currentChangeId or some other minor updates. But it's pretty hard to know what exactly needs to be // updated to correctly handle the reload, so it's safer to just reinitialize the whole material. @@ -269,7 +270,7 @@ namespace AZ void Material::OnShaderVariantReinitialized(const Shader& shader, const ShaderVariantId& /*shaderVariantId*/, ShaderVariantStableId shaderVariantStableId) { - ShaderReloadDebugTracker::ScopedSection reloadSection("Material::OnShaderVariantReinitialized %s variant %u", shader.GetAsset().GetHint().c_str(), shaderVariantStableId.GetIndex()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Material::OnShaderVariantReinitialized %s variant %u", this, shader.GetAsset().GetHint().c_str(), shaderVariantStableId.GetIndex()); // Note that it would be better to check the shaderVariantId to see if that variant is relevant to this particular material before reinitializing it. // There could be hundreds or even thousands of variants for a shader, but only one of those variants will be used by any given material. So we could diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp index 17ff2c64c8..8809225150 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp @@ -62,8 +62,6 @@ namespace AZ { AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); - m_aabb = modelAsset.GetAabb(); - m_lods.resize(modelAsset.GetLodAssets().size()); for (size_t lodIndex = 0; lodIndex < m_lods.size(); ++lodIndex) @@ -127,11 +125,6 @@ namespace AZ return m_isUploadPending; } - const AZ::Aabb& Model::GetAabb() const - { - return m_aabb; - } - const Data::Asset& Model::GetModelAsset() const { return m_modelAsset; @@ -140,9 +133,16 @@ namespace AZ bool Model::LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender); + + if (!GetModelAsset()) + { + AZ_Assert(false, "Invalid Model - not created from a ModelAsset?"); + return false; + } + float start; float end; - const int result = Intersect::IntersectRayAABB2(rayStart, rayDir.GetReciprocal(), m_aabb, start, end); + const int result = Intersect::IntersectRayAABB2(rayStart, rayDir.GetReciprocal(), GetModelAsset()->GetAabb(), start, end); if (Intersect::ISECT_RAY_AABB_NONE != result) { if (ModelAsset* modelAssetPtr = m_modelAsset.Get()) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp index b213e84bca..a4e5d1ba10 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp @@ -49,7 +49,7 @@ namespace AZ With that percentage we can determine which Lod we want to use. */ - Aabb modelAabb = model.GetAabb(); + Aabb modelAabb = model.GetModelAsset()->GetAabb(); modelAabb.Translate(position); Vector3 center; 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 99a6e23914..865a13e13d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp @@ -1036,6 +1036,26 @@ namespace AZ } } + void Pass::UpdateConnectedInputBindings() + { + for (uint8_t idx : m_inputBindingIndices) + { + UpdateConnectedBinding(m_attachmentBindings[idx]); + } + for (uint8_t idx : m_inputOutputBindingIndices) + { + UpdateConnectedBinding(m_attachmentBindings[idx]); + } + } + + void Pass::UpdateConnectedOutputBindings() + { + for (uint8_t idx : m_outputBindingIndices) + { + UpdateConnectedBinding(m_attachmentBindings[idx]); + } + } + // --- Queuing functions with PassSystem --- void Pass::QueueForBuildAndInitialization() @@ -1264,7 +1284,7 @@ namespace AZ AZ_Assert(m_state == PassState::Idle, "Pass::FrameBegin - Pass [%s] is attempting to render, but is not in the Idle state.", m_path.GetCStr()); m_state = PassState::Rendering; - UpdateConnectedBindings(); + UpdateConnectedInputBindings(); UpdateOwnedAttachments(); CreateTransientAttachments(params.m_frameGraphBuilder->GetAttachmentDatabase()); @@ -1273,6 +1293,8 @@ namespace AZ // FrameBeginInternal needs to be the last function be called in FrameBegin because its implementation expects // all the attachments are imported to database (for example, ImageAttachmentPreview) FrameBeginInternal(params); + + UpdateConnectedOutputBindings(); } void Pass::FrameEnd() diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Shader.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Shader.cpp index ab6e35e923..d9dcd163a6 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Shader.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Shader.cpp @@ -159,7 +159,7 @@ namespace AZ // AssetBus overrides void Shader::OnAssetReloaded(Data::Asset asset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("Shader::OnAssetReloaded %s", asset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Shader::OnAssetReloaded %s", this, asset.GetHint().c_str()); if (asset->GetId() == m_asset->GetId()) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp index 0afc174ad3..4d9e150a42 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp @@ -139,7 +139,7 @@ namespace AZ void MaterialAsset::OnAssetReloaded(Data::Asset asset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("MaterialAsset::OnAssetReloaded %s", asset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReloaded %s", this, asset.GetHint().c_str()); Data::Asset newMaterialTypeAsset = { asset.GetAs(), AZ::Data::AssetLoadBehavior::PreLoad }; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp index 145eedf2e6..b62ab02023 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp @@ -189,7 +189,7 @@ namespace AZ void MaterialTypeAsset::OnAssetReloaded(Data::Asset asset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("MaterialTypeAsset::OnAssetReloaded %s", asset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialTypeAsset::OnAssetReloaded %s", this, asset.GetHint().c_str()); // The order of asset reloads is non-deterministic. If the MaterialTypeAsset reloads before these // dependency assets, this will make sure the MaterialTypeAsset gets the latest ones when they reload. diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp index c89217460c..d989a0f145 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp @@ -581,7 +581,7 @@ namespace AZ // AssetBus overrides... void ShaderAsset::OnAssetReloaded(Data::Asset asset) { - ShaderReloadDebugTracker::ScopedSection reloadSection("ShaderAsset::OnAssetReloaded %s", asset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->ShaderAsset::OnAssetReloaded %s", this, asset.GetHint().c_str()); Data::Asset shaderVariantAsset = { asset.GetAs(), AZ::Data::AssetLoadBehavior::PreLoad }; AZ_Assert(shaderVariantAsset->GetStableId() == RootShaderVariantStableId, @@ -597,7 +597,7 @@ namespace AZ /// ShaderVariantFinderNotificationBus overrides void ShaderAsset::OnShaderVariantTreeAssetReady(Data::Asset shaderVariantTreeAsset, bool isError) { - ShaderReloadDebugTracker::ScopedSection reloadSection("ShaderAsset::OnShaderVariantTreeAssetReady %s", shaderVariantTreeAsset.GetHint().c_str()); + ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->ShaderAsset::OnShaderVariantTreeAssetReady %s", this, shaderVariantTreeAsset.GetHint().c_str()); AZStd::unique_lock lock(m_variantTreeMutex); if (isError) diff --git a/Gems/AtomContent/LookDevelopmentStudioPixar/Assets/Objects/PlayfulTeapot_playfulteapot.material b/Gems/AtomContent/LookDevelopmentStudioPixar/Assets/Objects/PlayfulTeapot_playfulteapot.material index 27540c587e..da35fda141 100644 --- a/Gems/AtomContent/LookDevelopmentStudioPixar/Assets/Objects/PlayfulTeapot_playfulteapot.material +++ b/Gems/AtomContent/LookDevelopmentStudioPixar/Assets/Objects/PlayfulTeapot_playfulteapot.material @@ -14,13 +14,14 @@ }, "clearCoat": { "enable": true, - "normalMap": "EngineAssets/Textures/perlinNoiseNormal_ddn.tif" + "normalMap": "EngineAssets/Textures/perlinNoiseNormal_ddn.tif", + "normalStrength": 0.10000000149011612 }, "general": { "applySpecularAA": true }, "metallic": { - "factor": 0.5 + "factor": 0.10000000149011612 }, "normal": { "factor": 0.05000000074505806, @@ -31,6 +32,9 @@ }, "roughness": { "factor": 0.0 + }, + "specularF0": { + "enableMultiScatterCompensation": true } } -} +} \ No newline at end of file diff --git a/Gems/AtomContent/gem.json b/Gems/AtomContent/gem.json index 941e7dea20..b043cbbaca 100644 --- a/Gems/AtomContent/gem.json +++ b/Gems/AtomContent/gem.json @@ -8,7 +8,7 @@ "Gem" ], "user_tags": [ - "AtomConent" + "AtomContent" ], "icon_path": "preview.png" } diff --git a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shadervariantlist b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shadervariantlist index 79bb726c9f..c7eddb10f2 100644 --- a/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shadervariantlist +++ b/Gems/AtomLyIntegration/AtomBridge/Assets/Shaders/LyShineUI.shadervariantlist @@ -4,7 +4,7 @@ { "StableId": 1, "Options": { - "o_preMultiplyAlpha": "true", + "o_preMultiplyAlpha": "false", "o_alphaTest": "false", "o_srgbWrite": "true", "o_modulate": "Modulate::None" @@ -14,7 +14,7 @@ "StableId": 2, "Options": { "o_preMultiplyAlpha": "false", - "o_alphaTest": "false", + "o_alphaTest": "true", "o_srgbWrite": "true", "o_modulate": "Modulate::None" } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Assets/LevelAssets/default.slice b/Gems/AtomLyIntegration/CommonFeatures/Assets/LevelAssets/default.slice index c20c6cde0b..b4c9eac10f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Assets/LevelAssets/default.slice +++ b/Gems/AtomLyIntegration/CommonFeatures/Assets/LevelAssets/default.slice @@ -960,7 +960,7 @@ - + @@ -968,10 +968,11 @@ - + + @@ -1109,4 +1110,3 @@ - diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Animation/EditorAttachmentComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Animation/EditorAttachmentComponent.cpp index f14340b4c9..e7f2a98a71 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Animation/EditorAttachmentComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Animation/EditorAttachmentComponent.cpp @@ -118,8 +118,7 @@ namespace AZ void EditorAttachmentComponent::Activate() { Base::Activate(); - m_boneFollower.Activate(GetEntity(), CreateAttachmentConfiguration(), - false); // Entity's don't animate in Editor + m_boneFollower.Activate(GetEntity(), CreateAttachmentConfiguration(), /*targetCanAnimate=*/true); } void EditorAttachmentComponent::Deactivate() diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp index 69bec21a6c..992b6319d8 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp @@ -112,13 +112,13 @@ namespace AZ ->DataElement(Edit::UIHandlers::Default, &AreaLightComponentConfig::m_enableShutters, "Enable shutters", "Restrict the light to a specific beam angle depending on shape.") ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::ShuttersMustBeEnabled) ->DataElement(Edit::UIHandlers::Slider, &AreaLightComponentConfig::m_innerShutterAngleDegrees, "Inner angle", "The inner angle of the shutters where the light beam begins to be occluded.") - ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 180.0f) + ->Attribute(Edit::Attributes::Min, 0.5f) + ->Attribute(Edit::Attributes::Max, 90.0f) ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShutters) ->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShuttersDisabled) ->DataElement(Edit::UIHandlers::Slider, &AreaLightComponentConfig::m_outerShutterAngleDegrees, "Outer angle", "The outer angle of the shutters where the light beam is completely occluded.") - ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 180.0f) + ->Attribute(Edit::Attributes::Min, 0.5f) + ->Attribute(Edit::Attributes::Max, 90.0f) ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShutters) ->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShuttersDisabled) diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationComponentConfig.h index fb99a99e1a..8f1b216af5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationComponentConfig.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/DiffuseGlobalIlluminationComponentConfig.h @@ -29,7 +29,7 @@ namespace AZ static void Reflect(ReflectContext* context); - DiffuseGlobalIlluminationQualityLevel m_qualityLevel; + DiffuseGlobalIlluminationQualityLevel m_qualityLevel = DiffuseGlobalIlluminationQualityLevel::Low; }; } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/EditorDiffuseGlobalIlluminationComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/EditorDiffuseGlobalIlluminationComponent.cpp index bdb5686e89..7df965d831 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/EditorDiffuseGlobalIlluminationComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/DiffuseGlobalIllumination/EditorDiffuseGlobalIlluminationComponent.cpp @@ -36,7 +36,7 @@ namespace AZ ->Attribute(Edit::Attributes::Category, "Atom") ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Component_Placeholder.svg") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Component_Placeholder.png") - ->Attribute(Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC("Level", 0x9aeacc13), AZ_CRC("Game", 0x232b318c) })) + ->Attribute(Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC("Level", 0x9aeacc13) })) ->Attribute(Edit::Attributes::AutoExpand, true) ->Attribute(Edit::Attributes::HelpPageURL, "https://") ; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp index e7eecd3c7f..90d3763067 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp @@ -460,10 +460,10 @@ namespace AZ Aabb MeshComponentController::GetLocalBounds() { - const Data::Instance model = GetModel(); - if (model) + if (m_meshHandle.IsValid() && m_meshFeatureProcessor) { - Aabb aabb = model->GetAabb(); + Aabb aabb = m_meshFeatureProcessor->GetLocalAabb(m_meshHandle); + aabb.MultiplyByScale(m_cachedNonUniformScale); return aabb; } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Thumbnails/Rendering/ThumbnailRendererSteps/CaptureStep.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Thumbnails/Rendering/ThumbnailRendererSteps/CaptureStep.cpp index 16baf16886..f2f33c4236 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Thumbnails/Rendering/ThumbnailRendererSteps/CaptureStep.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Thumbnails/Rendering/ThumbnailRendererSteps/CaptureStep.cpp @@ -96,6 +96,13 @@ namespace AZ RPI::AttachmentReadback::CallbackFunction readbackCallback = [&](const RPI::AttachmentReadback::ReadbackResult& result) { + if (!result.m_dataBuffer) + { + AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Event( + m_context->GetData()->m_thumbnailKeyRendered, + &AzToolsFramework::Thumbnailer::ThumbnailerRendererNotifications::ThumbnailFailedToRender); + return; + } uchar* data = result.m_dataBuffer.get()->data(); QImage image( data, result.m_imageDescriptor.m_size.m_width, result.m_imageDescriptor.m_size.m_height, QImage::Format_RGBA8888); diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp index 9f68a7d12c..1c4657d8df 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/ActorAsset.cpp @@ -311,7 +311,8 @@ namespace AZ Data::Asset modelAsset = actor->GetMeshAsset(); if (!modelAsset.IsReady()) { - AZ_Error("CreateSkinnedMeshInputFromActor", false, "Attempting to create skinned mesh input buffers for an actor that doesn't have a loaded model."); + AZ_Warning("CreateSkinnedMeshInputFromActor", false, "Check if the actor has a mesh added. Right click the source file in the asset browser, click edit settings, " + "and navigate to the Meshes tab. Add a mesh if it's missing."); return nullptr; } diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp index 532c8720b5..9337d5aad7 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp @@ -83,12 +83,20 @@ namespace AZ void AtomActorInstance::UpdateBounds() { // Update RenderActorInstance world bounding box - // The bounding box is moving with the actor instance. It is static in the way that it does not change shape. + // The bounding box is moving with the actor instance. // The entity and actor transforms are kept in sync already. m_worldAABB = AZ::Aabb::CreateFromMinMax(m_actorInstance->GetAABB().GetMin(), m_actorInstance->GetAABB().GetMax()); // Update RenderActorInstance local bounding box - m_localAABB = AZ::Aabb::CreateFromMinMax(m_actorInstance->GetStaticBasedAABB().GetMin(), m_actorInstance->GetStaticBasedAABB().GetMax()); + // NB: computing the local bbox from the world bbox makes the local bbox artifically larger than it should be + // instead EMFX should support getting the local bbox from the actor instance directly + m_localAABB = m_worldAABB.GetTransformedAabb(m_transformInterface->GetWorldTM().GetInverse()); + + // Update bbox on mesh instance if it exists + if (m_meshFeatureProcessor && m_meshHandle && m_meshHandle->IsValid() && m_skinnedMeshInstance) + { + m_meshFeatureProcessor->SetLocalAabb(*m_meshHandle, m_localAABB); + } AZ::Interface::Get()->RefreshEntityLocalBoundsUnion(m_entityId); } @@ -456,9 +464,8 @@ namespace AZ void AtomActorInstance::Create() { Destroy(); - m_skinnedMeshInputBuffers = GetRenderActor()->FindOrCreateSkinnedMeshInputBuffers(); - AZ_Error("AtomActorInstance", m_skinnedMeshInputBuffers, "Failed to get SkinnedMeshInputBuffers from Actor."); + AZ_Warning("AtomActorInstance", m_skinnedMeshInputBuffers, "Failed to create SkinnedMeshInputBuffers from Actor. It is likely that this actor doesn't have any meshes"); if (m_skinnedMeshInputBuffers) { m_boneTransforms = CreateBoneTransformBufferFromActorInstance(m_actorInstance, GetSkinningMethod()); diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr index d57e91f35d..aa7a8c4bd9 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr @@ -69,10 +69,16 @@ proj.launch-config = {loc('../../SDK/Atom/Scripts/Python/DCC_Materials/maya_mate loc('../../azpy/__init__.py'): ('custom', (u'', 'launch-oobMrvXFf1SwtYBg')), + loc('../../azpy/constants.py'): ('custom', + (u'', + 'launch-GeaM41WYMGA1sEfm')), loc('../../azpy/env_base.py'): ('project', (u'', 'launch-GeaM41WYMGA1sEfm')), loc('../../azpy/maya/callbacks/node_message_callback_handler.py'): ('c'\ 'ustom', + (u'', + 'launch-GeaM41WYMGA1sEfm')), + loc('../../config.py'): ('custom', (u'', 'launch-GeaM41WYMGA1sEfm'))} diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py index 0a0c6b8337..5b5c5aa079 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py @@ -165,12 +165,14 @@ def get_current_project(): bootstrap_box = None try: - bootstrap_box = Box.from_json(filename=PATH_USER_O3DE_BOOTSTRAP, + bootstrap_box = Box.from_json(filename=str(Path(PATH_USER_O3DE_BOOTSTRAP).resolve()), encoding="utf-8", errors="strict", object_pairs_hook=OrderedDict) - except FileExistsError as e: - _LOGGER.error('File does not exist: {}'.format(PATH_USER_O3DE_BOOTSTRAP)) + except Exception as e: + # this file runs in py2.7 for Maya 2020, FileExistsError is not defined + _LOGGER.error('FileExistsError: {}'.format(PATH_USER_O3DE_BOOTSTRAP)) + _LOGGER.error('exception is: {}'.format(e)) if bootstrap_box: # this seems fairly hard coded - what if the data changes? diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py index e10221d324..7f6b0e4523 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py @@ -226,7 +226,18 @@ TAG_DEFAULT_PY = str('Launch_pyBASE.bat') FILENAME_DEFAULT_CONFIG = str('DCCSI_config.json') # new o3de related paths -PATH_USER_O3DE = str('{home}\\{o3de}').format(home=expanduser("~"), +# os.path.expanduser("~") returns different values in py2.7 vs 3 +PATH_USER_HOME = expanduser("~") +_LOGGER.debug('user home: {}'.format(PATH_USER_HOME)) + +# special case, make sure didn't return \documents +parts = os.path.split(PATH_USER_HOME) + +if str(parts[1].lower()) == 'documents': + PATH_USER_HOME = parts[0] + _LOGGER.debug('user home CORRECTED: {}'.format(PATH_USER_HOME)) + +PATH_USER_O3DE = str('{home}\\{o3de}').format(home=PATH_USER_HOME, o3de=TAG_O3DE_FOLDER) PATH_USER_O3DE_REGISTRY = str('{0}\\Registry').format(PATH_USER_O3DE) PATH_USER_O3DE_BOOTSTRAP = str('{reg}\\{file}').format(reg=PATH_USER_O3DE_REGISTRY, diff --git a/Gems/Blast/Code/Source/Editor/EditorBlastFamilyComponent.cpp b/Gems/Blast/Code/Source/Editor/EditorBlastFamilyComponent.cpp index 873ef7248d..0133690cea 100644 --- a/Gems/Blast/Code/Source/Editor/EditorBlastFamilyComponent.cpp +++ b/Gems/Blast/Code/Source/Editor/EditorBlastFamilyComponent.cpp @@ -44,7 +44,7 @@ namespace Blast ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) ->Attribute( AZ::Edit::Attributes::HelpPageURL, - "https://docs.aws.amazon.com/lumberyard/latest/userguide/component-blast-actor.html") + "https://docs.o3de.org/docs/user-guide/components/reference/blast-family/") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement( AZ::Edit::UIHandlers::Default, &EditorBlastFamilyComponent::m_blastAsset, "Blast asset", diff --git a/Gems/Blast/Code/Source/Editor/EditorBlastMeshDataComponent.cpp b/Gems/Blast/Code/Source/Editor/EditorBlastMeshDataComponent.cpp index 77788b6aee..4a6f41331b 100644 --- a/Gems/Blast/Code/Source/Editor/EditorBlastMeshDataComponent.cpp +++ b/Gems/Blast/Code/Source/Editor/EditorBlastMeshDataComponent.cpp @@ -67,7 +67,7 @@ namespace Blast ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) ->Attribute( AZ::Edit::Attributes::HelpPageURL, - "https://docs.aws.amazon.com/lumberyard/latest/userguide/component-blast-actor.html") + "https://docs.o3de.org/docs/user-guide/components/reference/blast-family-mesh-data/") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement( AZ::Edit::UIHandlers::CheckBox, &EditorBlastMeshDataComponent::m_showMeshAssets, diff --git a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h index ca78623a8a..b1fea21550 100644 --- a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h +++ b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -221,23 +220,6 @@ namespace Blast AZStd::vector>(const Physics::MaterialSelection&)); MOCK_METHOD2( UpdateMaterialSelection, bool(const Physics::ShapeConfiguration&, Physics::ColliderConfiguration&)); - MOCK_METHOD0(GetSupportedJointTypes, AZStd::vector()); - MOCK_METHOD1(CreateJointLimitConfiguration, AZStd::shared_ptr(AZ::TypeId)); - MOCK_METHOD3( - CreateJoint, - AZStd::shared_ptr( - const AZStd::shared_ptr&, AzPhysics::SimulatedBody*, AzPhysics::SimulatedBody*)); - MOCK_METHOD10( - GenerateJointLimitVisualizationData, - void( - const Physics::JointLimitConfiguration&, const AZ::Quaternion&, const AZ::Quaternion&, float, AZ::u32, - AZ::u32, AZStd::vector&, AZStd::vector&, AZStd::vector&, - AZStd::vector&)); - MOCK_METHOD5( - ComputeInitialJointLimitConfiguration, - AZStd::unique_ptr( - const AZ::TypeId&, const AZ::Quaternion&, const AZ::Quaternion&, const AZ::Vector3&, - const AZStd::vector&)); MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string&, const AZ::Vector3*, AZ::u32)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3*, AZ::u32, AZStd::vector&)); MOCK_METHOD5( diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp index 22c875d8c3..864f80a2c5 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp @@ -71,12 +71,7 @@ namespace EMotionFX newNodeConfig.m_debugName = jointName; // Create joint limit on default. - AZStd::vector supportedJointLimitTypes; - Physics::SystemRequestBus::BroadcastResult(supportedJointLimitTypes, &Physics::SystemRequests::GetSupportedJointTypes); - if (!supportedJointLimitTypes.empty()) - { - newNodeConfig.m_jointLimit = CommandRagdollHelpers::CreateJointLimitByType(supportedJointLimitTypes[0], skeleton, joint); - } + newNodeConfig.m_jointConfig = CommandRagdollHelpers::CreateJointLimitByType(AzPhysics::JointType::D6Joint, skeleton, joint); if (index) { @@ -91,8 +86,8 @@ namespace EMotionFX } } - AZStd::unique_ptr CommandRagdollHelpers::CreateJointLimitByType( - const AZ::TypeId& typeId, const Skeleton* skeleton, const Node* node) + AZStd::unique_ptr CommandRagdollHelpers::CreateJointLimitByType( + AzPhysics::JointType jointType, const Skeleton* skeleton, const Node* node) { const Pose* bindPose = skeleton->GetBindPose(); const Transform& nodeBindTransform = bindPose->GetModelSpaceTransform(node->GetNodeIndex()); @@ -105,12 +100,20 @@ namespace EMotionFX AZ::Vector3 boneDirection = GetBoneDirection(skeleton, node); AZStd::vector exampleRotationsLocal; - AZStd::unique_ptr jointLimitConfig = - AZ::Interface::Get()->ComputeInitialJointLimitConfiguration( - typeId, parentBindRotationWorld, nodeBindRotationWorld, boneDirection, exampleRotationsLocal); + if (auto* jointHelpers = AZ::Interface::Get()) + { + if (AZStd::optional jointTypeId = jointHelpers->GetSupportedJointTypeId(jointType); + jointTypeId.has_value()) + { + AZStd::unique_ptr jointLimitConfig = jointHelpers->ComputeInitialJointLimitConfiguration( + *jointTypeId, parentBindRotationWorld, nodeBindRotationWorld, boneDirection, exampleRotationsLocal); - AZ_Assert(jointLimitConfig, "Could not create joint limit configuration with type '%s'.", typeId.ToString().c_str()); - return jointLimitConfig; + AZ_Assert(jointLimitConfig, "Could not create joint limit configuration."); + return jointLimitConfig; + } + } + AZ_Assert(false, "Could not create joint limit configuration."); + return nullptr; } void CommandRagdollHelpers::AddJointsToRagdoll(AZ::u32 actorId, const AZStd::vector& jointNames, @@ -532,7 +535,7 @@ namespace EMotionFX if (m_serializedJointLimits) { AZ::Outcome oldSerializedJointLimits = SerializeJointLimits(nodeConfig); - success |= MCore::ReflectionSerializer::DeserializeMembers(nodeConfig->m_jointLimit.get(), m_serializedJointLimits.value()); + success |= MCore::ReflectionSerializer::DeserializeMembers(nodeConfig->m_jointConfig.get(), m_serializedJointLimits.value()); if (success && oldSerializedJointLimits.IsSuccess()) { m_oldSerializedJointLimits = oldSerializedJointLimits.GetValue(); @@ -565,7 +568,7 @@ namespace EMotionFX AZ::Outcome CommandAdjustRagdollJoint::SerializeJointLimits(const Physics::RagdollNodeConfiguration* ragdollNodeConfig) { return MCore::ReflectionSerializer::SerializeMembersExcept( - ragdollNodeConfig->m_jointLimit.get(), + ragdollNodeConfig->m_jointConfig.get(), {"ParentLocalRotation", "ParentLocalPosition", "ChildLocalRotation", "ChildLocalPosition", } ); } diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.h b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.h index 8e94f028db..36683c82b4 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.h +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.h @@ -45,8 +45,8 @@ namespace EMotionFX Physics::RagdollConfiguration& ragdollConfig, const AZStd::optional& index, AZStd::string& outResult); - static AZStd::unique_ptr CreateJointLimitByType(const AZ::TypeId& typeId, - const Skeleton* skeleton, const Node* node); + static AZStd::unique_ptr CreateJointLimitByType( + AzPhysics::JointType jointType, const Skeleton* skeleton, const Node* node); static void AddJointsToRagdoll(AZ::u32 actorId, const AZStd::vector& jointNames, MCore::CommandGroup* commandGroup = nullptr, bool executeInsideCommand = false, bool addDefaultCollider = true); diff --git a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.cpp b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.cpp index 934bb113d5..96464de230 100644 --- a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.cpp +++ b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.cpp @@ -90,12 +90,15 @@ namespace EMotionFX if (serializeContext) { - AZStd::vector supportedJointLimitTypes; - Physics::SystemRequestBus::BroadcastResult(supportedJointLimitTypes, &Physics::SystemRequests::GetSupportedJointTypes); - for (const AZ::TypeId& jointLimitType : supportedJointLimitTypes) + //D6 joint is the only currently supported joint for ragdoll + if (auto* jointHelpers = AZ::Interface::Get()) { - const char* jointLimitName = serializeContext->FindClassData(jointLimitType)->m_editData->m_name; - m_typeComboBox->addItem(jointLimitName, jointLimitType.ToString().c_str()); + if (AZStd::optional d6jointTypeId = jointHelpers->GetSupportedJointTypeId(AzPhysics::JointType::D6Joint); + d6jointTypeId.has_value()) + { + const char* jointLimitName = serializeContext->FindClassData(*d6jointTypeId)->m_editData->m_name; + m_typeComboBox->addItem(jointLimitName, (*d6jointTypeId).ToString().c_str()); + } } // Reflected property editor for joint limit @@ -134,7 +137,7 @@ namespace EMotionFX Physics::RagdollNodeConfiguration* ragdollNodeConfig = GetRagdollNodeConfig(); if (ragdollNodeConfig) { - Physics::JointLimitConfiguration* jointLimitConfig = ragdollNodeConfig->m_jointLimit.get(); + AzPhysics::JointConfiguration* jointLimitConfig = ragdollNodeConfig->m_jointConfig.get(); if (jointLimitConfig) { const AZ::TypeId& jointTypeId = jointLimitConfig->RTTI_GetType(); @@ -262,13 +265,14 @@ namespace EMotionFX { if (type.IsNull()) { - ragdollNodeConfig->m_jointLimit = nullptr; + ragdollNodeConfig->m_jointConfig = nullptr; } else { const Node* node = m_nodeIndex.data(SkeletonModel::ROLE_POINTER).value(); const Skeleton* skeleton = m_nodeIndex.data(SkeletonModel::ROLE_ACTOR_POINTER).value()->GetSkeleton(); - ragdollNodeConfig->m_jointLimit = CommandRagdollHelpers::CreateJointLimitByType(type, skeleton, node); + ragdollNodeConfig->m_jointConfig = + CommandRagdollHelpers::CreateJointLimitByType(AzPhysics::JointType::D6Joint, skeleton, node); } Update(); diff --git a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.h b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.h index 211c70e9bf..7861334239 100644 --- a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.h +++ b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollJointLimitWidget.h @@ -13,7 +13,6 @@ #pragma once #if !defined(Q_MOC_RUN) -#include #include #include #include diff --git a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.cpp b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.cpp index e2fde04016..467c56f16a 100644 --- a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.cpp +++ b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.cpp @@ -514,7 +514,7 @@ namespace EMotionFX if (renderJointLimits && jointSelected) { - const AZStd::shared_ptr& jointLimitConfig = ragdollNode.m_jointLimit; + const AZStd::shared_ptr& jointLimitConfig = ragdollNode.m_jointConfig; if (jointLimitConfig) { const Node* ragdollParentNode = physicsSetup->FindRagdollParentNode(joint); @@ -528,7 +528,8 @@ namespace EMotionFX } } - void RagdollNodeInspectorPlugin::RenderJointLimit(const Physics::JointLimitConfiguration& configuration, + void RagdollNodeInspectorPlugin::RenderJointLimit( + const AzPhysics::JointConfiguration& configuration, const ActorInstance* actorInstance, const Node* node, const Node* parentNode, @@ -549,9 +550,12 @@ namespace EMotionFX m_indexBuffer.clear(); m_lineBuffer.clear(); m_lineValidityBuffer.clear(); - Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::GenerateJointLimitVisualizationData, - configuration, parentOrientation, childOrientation, s_scale, s_angularSubdivisions, s_radialSubdivisions, - m_vertexBuffer, m_indexBuffer, m_lineBuffer, m_lineValidityBuffer); + if(auto* jointHelpers = AZ::Interface::Get()) + { + jointHelpers->GenerateJointLimitVisualizationData( + configuration, parentOrientation, childOrientation, s_scale, s_angularSubdivisions, s_radialSubdivisions, m_vertexBuffer, + m_indexBuffer, m_lineBuffer, m_lineValidityBuffer); + } Transform jointModelSpaceTransform = currentPose->GetModelSpaceTransform(parentNodeIndex); jointModelSpaceTransform.mPosition = currentPose->GetModelSpaceTransform(nodeIndex).mPosition; @@ -572,7 +576,8 @@ namespace EMotionFX } } - void RagdollNodeInspectorPlugin::RenderJointFrame(const Physics::JointLimitConfiguration& configuration, + void RagdollNodeInspectorPlugin::RenderJointFrame( + const AzPhysics::JointConfiguration& configuration, const ActorInstance* actorInstance, const Node* node, const Node* parentNode, diff --git a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.h b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.h index f7aa1277b0..7bba1b0b4f 100644 --- a/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.h +++ b/Gems/EMotionFX/Code/Source/Editor/Plugins/Ragdoll/RagdollNodeInspectorPlugin.h @@ -60,14 +60,16 @@ namespace EMotionFX void Render(EMStudio::RenderPlugin* renderPlugin, RenderInfo* renderInfo) override; void RenderRagdoll(ActorInstance* actorInstance, bool renderColliders, bool renderJointLimits, EMStudio::RenderPlugin* renderPlugin, RenderInfo* renderInfo); - void RenderJointLimit(const Physics::JointLimitConfiguration& jointConfiguration, + void RenderJointLimit( + const AzPhysics::JointConfiguration& jointConfiguration, const ActorInstance* actorInstance, const Node* node, const Node* parentNode, EMStudio::RenderPlugin* renderPlugin, EMStudio::EMStudioPlugin::RenderInfo* renderInfo, const MCore::RGBAColor& color); - void RenderJointFrame(const Physics::JointLimitConfiguration& jointConfiguration, + void RenderJointFrame( + const AzPhysics::JointConfiguration& jointConfiguration, const ActorInstance* actorInstance, const Node* node, const Node* parentNode, diff --git a/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp b/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp index 157e8c0136..4ba6236a36 100644 --- a/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp +++ b/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp @@ -508,8 +508,7 @@ namespace EMotionFX void EditorActorComponent::OnAssetReady(AZ::Data::Asset asset) { m_actorAsset = asset; - Actor* actor = m_actorAsset->GetActor(); - AZ_Assert(m_actorAsset.IsReady() && actor, "Actor asset should be loaded and actor valid."); + AZ_Assert(m_actorAsset.IsReady() && m_actorAsset->GetActor(), "Actor asset should be loaded and actor valid."); CheckActorCreation(); } diff --git a/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.cpp b/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.cpp index 417946524d..73c0a9fdae 100644 --- a/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.cpp +++ b/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.cpp @@ -19,7 +19,7 @@ namespace EMotionFX { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(1) ->Field("SwingLimitY", &D6JointLimitConfiguration::m_swingLimitY) ->Field("SwingLimitZ", &D6JointLimitConfiguration::m_swingLimitZ) diff --git a/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.h b/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.h index 47b6b4bef4..b41ba6435b 100644 --- a/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.h +++ b/Gems/EMotionFX/Code/Tests/D6JointLimitConfiguration.h @@ -12,23 +12,22 @@ #pragma once -#include +#include namespace EMotionFX { // Add so that RagdollNodeInspectorPlugin::PhysXCharactersGemAvailable() will return the correct value // We duplicated the D6JointLimitConfiguration because it doesn't exist in the test environment. class D6JointLimitConfiguration - : public Physics::JointLimitConfiguration + : public AzPhysics::JointConfiguration { public: AZ_CLASS_ALLOCATOR(D6JointLimitConfiguration, AZ::SystemAllocator, 0); // This uses the same uuid as the production D6JointLimitConfiguration. // The Ragdoll UI uses this UUID to see if physx is available. - AZ_RTTI(D6JointLimitConfiguration, "{90C5C23D-16C0-4F23-AD50-A190E402388E}", Physics::JointLimitConfiguration); + AZ_RTTI(D6JointLimitConfiguration, "{90C5C23D-16C0-4F23-AD50-A190E402388E}", AzPhysics::JointConfiguration); static void Reflect(AZ::ReflectContext* context); - const char* GetTypeName() override { return "D6 Joint"; } float m_swingLimitY = 45.0f; ///< Maximum angle in degrees from the Y axis of the joint frame. float m_swingLimitZ = 45.0f; ///< Maximum angle in degrees from the Z axis of the joint frame. diff --git a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h index 3aafdb4e2b..2ad3853edb 100644 --- a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h +++ b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h @@ -35,11 +35,6 @@ namespace Physics MOCK_METHOD2(CreateShape, AZStd::shared_ptr(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration& materialConfiguration)); - MOCK_METHOD0(GetSupportedJointTypes, AZStd::vector()); - MOCK_METHOD1(CreateJointLimitConfiguration, AZStd::shared_ptr(AZ::TypeId jointType)); - MOCK_METHOD3(CreateJoint, AZStd::shared_ptr(const AZStd::shared_ptr& configuration, AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody)); - MOCK_METHOD10(GenerateJointLimitVisualizationData, void(const Physics::JointLimitConfiguration& configuration, const AZ::Quaternion& parentRotation, const AZ::Quaternion& childRotation, float scale, AZ::u32 angularSubdivisions, AZ::u32 radialSubdivisions, AZStd::vector& vertexBufferOut, AZStd::vector& indexBufferOut, AZStd::vector& lineBufferOut, AZStd::vector& lineValidityBufferOut)); - MOCK_METHOD5(ComputeInitialJointLimitConfiguration, AZStd::unique_ptr(const AZ::TypeId& jointLimitTypeId, const AZ::Quaternion& parentWorldRotation, const AZ::Quaternion& childWorldRotation, const AZ::Vector3& axis, const AZStd::vector& exampleLocalRotations)); MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector& result)); MOCK_METHOD5(CookTriangleMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount, const AZ::u32* indices, AZ::u32 indexCount)); @@ -72,6 +67,36 @@ namespace Physics MOCK_CONST_METHOD0(GetDefaultSceneConfiguration, const AzPhysics::SceneConfiguration& ()); }; + class MockJointHelpersInterface : AZ::Interface::Registrar + { + public: + MOCK_CONST_METHOD0(GetSupportedJointTypeIds, const AZStd::vector()); + MOCK_CONST_METHOD1(GetSupportedJointTypeId, AZStd::optional(AzPhysics::JointType typeEnum)); + + MOCK_METHOD5( + ComputeInitialJointLimitConfiguration, + AZStd::unique_ptr( + const AZ::TypeId& jointLimitTypeId, + const AZ::Quaternion& parentWorldRotation, + const AZ::Quaternion& childWorldRotation, + const AZ::Vector3& axis, + const AZStd::vector& exampleLocalRotations)); + + MOCK_METHOD10( + GenerateJointLimitVisualizationData, + void( + const AzPhysics::JointConfiguration& configuration, + const AZ::Quaternion& parentRotation, + const AZ::Quaternion& childRotation, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& vertexBufferOut, + AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut)); + }; + //Mocked of the AzPhysics Scene Interface. To keep things simple just mocked functions that have a return value OR required for a test. class MockPhysicsSceneInterface : AZ::Interface::Registrar @@ -97,6 +122,9 @@ namespace Physics void DisableSimulationOfBody( [[maybe_unused]] AzPhysics::SceneHandle sceneHandle, [[maybe_unused]] AzPhysics::SimulatedBodyHandle bodyHandle) override {} + void RemoveJoint( + [[maybe_unused]]AzPhysics::SceneHandle sceneHandle, + [[maybe_unused]] AzPhysics::JointHandle jointHandle) override {} void SuppressCollisionEvents( [[maybe_unused]] AzPhysics::SceneHandle sceneHandle, [[maybe_unused]] const AzPhysics::SimulatedBodyHandle& bodyHandleA, @@ -145,6 +173,9 @@ namespace Physics MOCK_METHOD2(AddSimulatedBodies, AzPhysics::SimulatedBodyHandleList(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SimulatedBodyConfigurationList& simulatedBodyConfigs)); MOCK_METHOD2(GetSimulatedBodyFromHandle, AzPhysics::SimulatedBody* (AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle)); MOCK_METHOD2(GetSimulatedBodiesFromHandle, AzPhysics::SimulatedBodyList(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SimulatedBodyHandleList& bodyHandles)); + MOCK_METHOD4(AddJoint, AzPhysics::JointHandle(AzPhysics::SceneHandle sceneHandle, const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody)); + MOCK_METHOD2(GetJointFromHandle, AzPhysics::Joint* (AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle bodyHandle)); MOCK_CONST_METHOD1(GetGravity, AZ::Vector3(AzPhysics::SceneHandle sceneHandle)); MOCK_METHOD2(RegisterSceneSimulationFinishHandler, void(AzPhysics::SceneHandle sceneHandle, AzPhysics::SceneEvents::OnSceneSimulationFinishHandler& handler)); MOCK_CONST_METHOD2(GetLegacyBody, AzPhysics::SimulatedBody* (AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle handle)); diff --git a/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteColliders.cpp b/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteColliders.cpp index eaef6f31ee..7e5f481453 100644 --- a/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteColliders.cpp +++ b/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteColliders.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -44,10 +45,21 @@ namespace EMotionFX D6JointLimitConfiguration::Reflect(GetSerializeContext()); - const AZ::TypeId& jointLimitTypeId = azrtti_typeid(); - EXPECT_CALL(m_physicsSystem, GetSupportedJointTypes) - .WillRepeatedly(testing::Return(AZStd::vector{jointLimitTypeId})); - EXPECT_CALL(m_physicsSystem, ComputeInitialJointLimitConfiguration(jointLimitTypeId, _, _, _, _)) + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeIds) + .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); + + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeId(_)) + .WillRepeatedly( + [](AzPhysics::JointType jointType) -> AZStd::optional + { + if (jointType == AzPhysics::JointType::D6Joint) + { + return azrtti_typeid(); + } + return AZStd::nullopt; + }); + + EXPECT_CALL(m_jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) .WillRepeatedly([]([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, [[maybe_unused]] const AZ::Quaternion& childWorldRotation, @@ -59,6 +71,7 @@ namespace EMotionFX private: Physics::MockPhysicsSystem m_physicsSystem; Physics::MockPhysicsInterface m_physicsInterface; + Physics::MockJointHelpersInterface m_jointHelpers; }; #if AZ_TRAIT_DISABLE_FAILED_EMOTION_FX_EDITOR_TESTS diff --git a/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteJointLimits.cpp b/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteJointLimits.cpp index 6223af3599..4f65220400 100644 --- a/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteJointLimits.cpp +++ b/Gems/EMotionFX/Code/Tests/ProvidesUI/Ragdoll/CanCopyPasteJointLimits.cpp @@ -55,10 +55,27 @@ namespace EMotionFX Physics::MockPhysicsSystem physicsSystem; Physics::MockPhysicsInterface physicsInterface; - EXPECT_CALL(physicsSystem, GetSupportedJointTypes) - .WillRepeatedly(testing::Return(AZStd::vector{azrtti_typeid()})); - EXPECT_CALL(physicsSystem, ComputeInitialJointLimitConfiguration(azrtti_typeid(), _, _, _, _)) - .WillRepeatedly([]([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, [[maybe_unused]] const AZ::Quaternion& childWorldRotation, [[maybe_unused]] const AZ::Vector3& axis, [[maybe_unused]] const AZStd::vector& exampleLocalRotations) { return AZStd::make_unique(); }); + Physics::MockJointHelpersInterface jointHelpers; + EXPECT_CALL(jointHelpers, GetSupportedJointTypeIds) + .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); + EXPECT_CALL(jointHelpers, GetSupportedJointTypeId(_)) + .WillRepeatedly( + [](AzPhysics::JointType jointType) -> AZStd::optional + { + if (jointType == AzPhysics::JointType::D6Joint) + { + return azrtti_typeid(); + } + return AZStd::nullopt; + }); + EXPECT_CALL(jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) + .WillRepeatedly( + []([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, + [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, [[maybe_unused]] const AZ::Quaternion& childWorldRotation, + [[maybe_unused]] const AZ::Vector3& axis, [[maybe_unused]] const AZStd::vector& exampleLocalRotations) + { + return AZStd::make_unique(); + }); AutoRegisteredActor actor {ActorFactory::CreateAndInit(4)}; @@ -70,7 +87,7 @@ namespace EMotionFX CommandRagdollHelpers::AddJointsToRagdoll(actor->GetID(), {"rootJoint", "joint1", "joint2", "joint3"}); const Physics::RagdollConfiguration& ragdollConfig = actor->GetPhysicsSetup()->GetRagdollConfig(); ASSERT_EQ(ragdollConfig.m_nodes.size(), 4); - AZStd::shared_ptr rootJointLimit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("rootJoint")->m_jointLimit); + AZStd::shared_ptr rootJointLimit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("rootJoint")->m_jointConfig); ASSERT_TRUE(rootJointLimit); // Set the initial joint limits on the rootJoint to be something different rootJointLimit->m_swingLimitY = 1.0f; @@ -119,7 +136,8 @@ namespace EMotionFX selectionModel.select(joint1Index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); // Verify initial state of joint1's limits - const AZStd::shared_ptr joint1Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint1")->m_jointLimit); + const AZStd::shared_ptr joint1Limit = + AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint1")->m_jointConfig); EXPECT_EQ(joint1Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint1Limit->m_swingLimitZ, 45.0f); @@ -141,10 +159,12 @@ namespace EMotionFX selectionModel.select(joint2Index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); selectionModel.select(joint3Index, QItemSelectionModel::Select | QItemSelectionModel::Rows); - const AZStd::shared_ptr joint2Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint2")->m_jointLimit); + const AZStd::shared_ptr joint2Limit = + AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint2")->m_jointConfig); EXPECT_EQ(joint2Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint2Limit->m_swingLimitZ, 45.0f); - const AZStd::shared_ptr joint3Limit = AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint3")->m_jointLimit); + const AZStd::shared_ptr joint3Limit = + AZStd::rtti_pointer_cast(ragdollConfig.FindNodeConfigByName("joint3")->m_jointConfig); EXPECT_EQ(joint3Limit->m_swingLimitY, 45.0f); EXPECT_EQ(joint3Limit->m_swingLimitZ, 45.0f); diff --git a/Gems/EMotionFX/Code/Tests/RagdollCommandTests.cpp b/Gems/EMotionFX/Code/Tests/RagdollCommandTests.cpp index 279d3ff38c..f78dc417ac 100644 --- a/Gems/EMotionFX/Code/Tests/RagdollCommandTests.cpp +++ b/Gems/EMotionFX/Code/Tests/RagdollCommandTests.cpp @@ -18,11 +18,47 @@ #include #include #include - +#include +#include namespace EMotionFX { - using RagdollCommandTests = ActorFixture; + class RagdollCommandTests : public ActorFixture + { + public: + void SetUp() override + { + using ::testing::_; + + ActorFixture::SetUp(); + + D6JointLimitConfiguration::Reflect(GetSerializeContext()); + + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeIds) + .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); + + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeId(_)) + .WillRepeatedly( + [](AzPhysics::JointType jointType) -> AZStd::optional + { + if (jointType == AzPhysics::JointType::D6Joint) + { + return azrtti_typeid(); + } + return AZStd::nullopt; + }); + + EXPECT_CALL(m_jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) + .WillRepeatedly([]([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, + [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, + [[maybe_unused]] const AZ::Quaternion& childWorldRotation, + [[maybe_unused]] const AZ::Vector3& axis, + [[maybe_unused]] const AZStd::vector& exampleLocalRotations) + { return AZStd::make_unique(); }); + } + protected: + Physics::MockJointHelpersInterface m_jointHelpers; + }; AZStd::vector GetRagdollJointNames(const Actor* actor) { diff --git a/Gems/EMotionFX/Code/Tests/UI/CanAddToSimulatedObject.cpp b/Gems/EMotionFX/Code/Tests/UI/CanAddToSimulatedObject.cpp index 50fb434a26..8b75cfbf37 100644 --- a/Gems/EMotionFX/Code/Tests/UI/CanAddToSimulatedObject.cpp +++ b/Gems/EMotionFX/Code/Tests/UI/CanAddToSimulatedObject.cpp @@ -10,6 +10,7 @@ * */ +#include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include namespace EMotionFX { @@ -140,6 +142,31 @@ namespace EMotionFX TEST_F(CanAddToSimulatedObjectFixture, CanAddCollidersfromRagdoll) { + using ::testing::_; + Physics::MockJointHelpersInterface jointHelpers; + EXPECT_CALL(jointHelpers, GetSupportedJointTypeIds) + .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); + + EXPECT_CALL(jointHelpers, GetSupportedJointTypeId(_)) + .WillRepeatedly( + [](AzPhysics::JointType jointType) -> AZStd::optional + { + if (jointType == AzPhysics::JointType::D6Joint) + { + return azrtti_typeid(); + } + return AZStd::nullopt; + }); + + EXPECT_CALL(jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) + .WillRepeatedly( + []([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, + [[maybe_unused]] const AZ::Quaternion& childWorldRotation, [[maybe_unused]] const AZ::Vector3& axis, + [[maybe_unused]] const AZStd::vector& exampleLocalRotations) + { + return AZStd::make_unique(); + }); + RecordProperty("test_case_id", "C13291807"); AutoRegisteredActor actor = ActorFactory::CreateAndInit(7, "CanAddToSimulatedObjectActor"); diff --git a/Gems/EMotionFX/Code/Tests/UI/RagdollEditTests.cpp b/Gems/EMotionFX/Code/Tests/UI/RagdollEditTests.cpp index bd30ac378d..9cc2a3add2 100644 --- a/Gems/EMotionFX/Code/Tests/UI/RagdollEditTests.cpp +++ b/Gems/EMotionFX/Code/Tests/UI/RagdollEditTests.cpp @@ -10,6 +10,7 @@ * */ +#include #include #include @@ -27,6 +28,7 @@ #include #include +#include namespace EMotionFX { @@ -35,6 +37,8 @@ namespace EMotionFX public: void SetUp() override { + using ::testing::_; + SetupQtAndFixtureBase(); AZ::SerializeContext* serializeContext = nullptr; @@ -42,6 +46,29 @@ namespace EMotionFX D6JointLimitConfiguration::Reflect(serializeContext); + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeIds) + .WillRepeatedly(testing::Return(AZStd::vector{ azrtti_typeid() })); + + EXPECT_CALL(m_jointHelpers, GetSupportedJointTypeId(_)) + .WillRepeatedly( + [](AzPhysics::JointType jointType) -> AZStd::optional + { + if (jointType == AzPhysics::JointType::D6Joint) + { + return azrtti_typeid(); + } + return AZStd::nullopt; + }); + + EXPECT_CALL(m_jointHelpers, ComputeInitialJointLimitConfiguration(_, _, _, _, _)) + .WillRepeatedly( + []([[maybe_unused]] const AZ::TypeId& jointLimitTypeId, [[maybe_unused]] const AZ::Quaternion& parentWorldRotation, + [[maybe_unused]] const AZ::Quaternion& childWorldRotation, [[maybe_unused]] const AZ::Vector3& axis, + [[maybe_unused]] const AZStd::vector& exampleLocalRotations) + { + return AZStd::make_unique(); + }); + SetupPluginWindows(); } @@ -73,6 +100,7 @@ namespace EMotionFX QModelIndexList m_indexList; ReselectingTreeView* m_treeView; EMotionFX::SkeletonOutlinerPlugin* m_skeletonOutliner; + Physics::MockJointHelpersInterface m_jointHelpers; }; diff --git a/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake b/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake index e2670c20f7..58366f8ffb 100644 --- a/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake +++ b/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake @@ -63,8 +63,6 @@ set(FILES Tests/Integration/CanDeleteJackEntity.cpp Tests/Bugs/CanDeleteMotionWhenMotionIsBeingBlended.cpp Tests/Bugs/CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp - Tests/D6JointLimitConfiguration.cpp - Tests/D6JointLimitConfiguration.h Tests/Editor/FileManagerTests.cpp Tests/Editor/ParametersGroupDefaultValues.cpp Tests/Editor/MotionSetLoadEscalation.cpp diff --git a/Gems/EMotionFX/Code/emotionfx_shared_tests_files.cmake b/Gems/EMotionFX/Code/emotionfx_shared_tests_files.cmake index 968dbd0f52..f1a0be0b8d 100644 --- a/Gems/EMotionFX/Code/emotionfx_shared_tests_files.cmake +++ b/Gems/EMotionFX/Code/emotionfx_shared_tests_files.cmake @@ -21,4 +21,6 @@ set(FILES Tests/TestAssetCode/AnimGraphAssetFactory.h Tests/TestAssetCode/ActorAssetFactory.h Tests/TestAssetCode/MotionSetAssetFactory.h + Tests/D6JointLimitConfiguration.cpp + Tests/D6JointLimitConfiguration.h ) diff --git a/Gems/LyShine/Code/Source/RenderGraph.cpp b/Gems/LyShine/Code/Source/RenderGraph.cpp index d5a1df7b15..ad2c46f8e6 100644 --- a/Gems/LyShine/Code/Source/RenderGraph.cpp +++ b/Gems/LyShine/Code/Source/RenderGraph.cpp @@ -137,7 +137,11 @@ namespace LyShine AZ::RHI::Ptr dynamicDraw = uiRenderer->GetDynamicDrawContext(); const UiRenderer::UiShaderData& uiShaderData = uiRenderer->GetUiShaderData(); - dynamicDraw->SetShaderVariant(uiShaderData.m_shaderVariantDefault); + // Set render state + dynamicDraw->SetStencilState(uiRenderer->GetBaseState().m_stencilState); + dynamicDraw->SetTarget0BlendState(uiRenderer->GetBaseState().m_blendState); + + dynamicDraw->SetShaderVariant(uiRenderer->GetCurrentShaderVariant()); // Set up per draw SRG AZ::Data::Instance drawSrg = dynamicDraw->NewDrawSrg(); @@ -307,7 +311,7 @@ namespace LyShine //////////////////////////////////////////////////////////////////////////////////////////////////// void MaskRenderNode::Render(UiRenderer* uiRenderer) { - int priorBaseState = uiRenderer->GetBaseState(); + UiRenderer::BaseState priorBaseState = uiRenderer->GetBaseState(); if (m_isMaskingEnabled || m_drawBehind) { @@ -369,68 +373,61 @@ namespace LyShine #endif //////////////////////////////////////////////////////////////////////////////////////////////////// - void MaskRenderNode::SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState) + void MaskRenderNode::SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState) { + UiRenderer::BaseState curBaseState = priorBaseState; + // If using alpha test for drawing the renderable components on this element then we turn on // alpha test as a pre-render step - int alphaTest = 0; - if (m_useAlphaTest) - { - alphaTest = GS_ALPHATEST_GREATER; - } + curBaseState.m_useAlphaTest = m_useAlphaTest; // if either of the draw flags are checked then we may want to draw the renderable component(s) // on this element, otherwise use the color mask to stop them rendering - int colorMask = GS_COLMASK_NONE; + curBaseState.m_blendState.m_enable = false; + curBaseState.m_blendState.m_writeMask = 0x0; if ((m_drawBehind && firstPass) || (m_drawInFront && !firstPass)) { - colorMask = 0; // mask everything, don't write color or alpha, we just write to stencil buffer + curBaseState.m_blendState.m_enable = true; + curBaseState.m_blendState.m_writeMask = 0xF; } - if (m_isMaskingEnabled) + if (m_isMaskingEnabled) { + AZ::RHI::StencilOpState stencilOpState; + stencilOpState.m_func = AZ::RHI::ComparisonFunc::Equal; + // masking is enabled so we want to setup to increment (first pass) or decrement (second pass) // the stencil buff when rendering the renderable component(s) on this element - int passOp = 0; if (firstPass) { - passOp = STENCOP_PASS(FSS_STENCOP_INCR); - gEnv->pRenderer->PushProfileMarker(s_maskIncrProfileMarker); + stencilOpState.m_passOp = AZ::RHI::StencilOp::Increment; } else { - passOp = STENCOP_PASS(FSS_STENCOP_DECR); - gEnv->pRenderer->PushProfileMarker(s_maskDecrProfileMarker); + stencilOpState.m_passOp = AZ::RHI::StencilOp::Decrement; } + curBaseState.m_stencilState.m_frontFace = stencilOpState; + curBaseState.m_stencilState.m_backFace = stencilOpState; + // set up for stencil write - const uint32 stencilRef = uiRenderer->GetStencilRef(); - const uint32 stencilMask = 0xFF; - const uint32 stencilWriteMask = 0xFF; - const int32 stencilState = STENC_FUNC(FSS_STENCFUNC_EQUAL) - | STENCOP_FAIL(FSS_STENCOP_KEEP) | STENCOP_ZFAIL(FSS_STENCOP_KEEP) | passOp; - gEnv->pRenderer->SetStencilState(stencilState, stencilRef, stencilMask, stencilWriteMask); - - // Set the base state that should be used when rendering the renderable component(s) on this - // element - uiRenderer->SetBaseState(priorBaseState | GS_STENCIL | alphaTest | colorMask); + AZ::RHI::Ptr dynamicDraw = uiRenderer->GetDynamicDrawContext(); + dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef()); + curBaseState.m_stencilState.m_enable = true; + curBaseState.m_stencilState.m_writeMask = 0xFF; } else { // masking is not enabled - - // Even if not masking we still use alpha test (if checked). This is primarily to help the user to - // visualize what their alpha tested mask looks like. - if (colorMask || alphaTest) - { - uiRenderer->SetBaseState(priorBaseState | colorMask | alphaTest); - } + curBaseState.m_stencilState.m_enable = false; } + + uiRenderer->SetBaseState(curBaseState); } //////////////////////////////////////////////////////////////////////////////////////////////////// - void MaskRenderNode::SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState) + void MaskRenderNode::SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState) { if (m_isMaskingEnabled) { @@ -442,26 +439,29 @@ namespace LyShine if (firstPass) { uiRenderer->IncrementStencilRef(); - gEnv->pRenderer->PopProfileMarker(s_maskIncrProfileMarker); } else { uiRenderer->DecrementStencilRef(); - gEnv->pRenderer->PopProfileMarker(s_maskDecrProfileMarker); } - // turn off stencil write and turn on stencil test - const uint32 stencilRef = uiRenderer->GetStencilRef(); - const uint32 stencilMask = 0xFF; - const uint32 stencilWriteMask = 0x00; - const int32 stencilState = STENC_FUNC(FSS_STENCFUNC_EQUAL) - | STENCOP_FAIL(FSS_STENCOP_KEEP) | STENCOP_ZFAIL(FSS_STENCOP_KEEP) | STENCOP_PASS(FSS_STENCOP_KEEP); - gEnv->pRenderer->SetStencilState(stencilState, stencilRef, stencilMask, stencilWriteMask); + AZ::RHI::Ptr dynamicDraw = uiRenderer->GetDynamicDrawContext(); + dynamicDraw->SetStencilReference(uiRenderer->GetStencilRef()); if (firstPass) { - // first pass, turn on stencil test for drawing children - uiRenderer->SetBaseState(priorBaseState | GS_STENCIL); + UiRenderer::BaseState curBaseState = priorBaseState; + + // turn off stencil write and turn on stencil test + curBaseState.m_stencilState.m_enable = true; + curBaseState.m_stencilState.m_writeMask = 0x00; + + AZ::RHI::StencilOpState stencilOpState; + stencilOpState.m_func = AZ::RHI::ComparisonFunc::Equal; + curBaseState.m_stencilState.m_frontFace = stencilOpState; + curBaseState.m_stencilState.m_backFace = stencilOpState; + + uiRenderer->SetBaseState(curBaseState); } else { @@ -475,7 +475,6 @@ namespace LyShine // remove any color mask or alpha test that we set in pre-render uiRenderer->SetBaseState(priorBaseState); } - } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -637,35 +636,24 @@ namespace LyShine //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::BeginMask(bool isMaskingEnabled, bool useAlphaTest, bool drawBehind, bool drawInFront) { -#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets) - // this uses pool allocator MaskRenderNode* maskRenderNode = new MaskRenderNode(m_currentMask, isMaskingEnabled, useAlphaTest, drawBehind, drawInFront); m_currentMask = maskRenderNode; m_renderNodeListStack.push(&maskRenderNode->GetMaskRenderNodeList()); -#else - AZ_UNUSED(drawInFront); - AZ_UNUSED(drawBehind); - AZ_UNUSED(useAlphaTest); - AZ_UNUSED(isMaskingEnabled); -#endif } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::StartChildrenForMask() { -#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets) AZ_Assert(m_currentMask, "Calling StartChildrenForMask while not defining a mask"); m_renderNodeListStack.pop(); m_renderNodeListStack.push(&m_currentMask->GetContentRenderNodeList()); -#endif } //////////////////////////////////////////////////////////////////////////////////////////////////// void RenderGraph::EndMask() { -#ifdef LYSHINE_ATOM_TODO // keeping this code for future phase (masks and render targets) AZ_Assert(m_currentMask, "Calling EndMask while not defining a mask"); if (m_currentMask) { @@ -686,7 +674,6 @@ namespace LyShine m_renderNodeListStack.top()->push_back(newMaskRenderNode); } } -#endif } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -996,6 +983,12 @@ namespace LyShine // LYSHINE_ATOM_TODO - will probably need to support this when converting UI Editor to use Atom AZ_UNUSED(viewportSize); + AZ::RHI::Ptr dynamicDraw = uiRenderer->GetDynamicDrawContext(); + + // Disable stencil and enable blend/color write + dynamicDraw->SetStencilState(uiRenderer->GetBaseState().m_stencilState); + dynamicDraw->SetTarget0BlendState(uiRenderer->GetBaseState().m_blendState); + // First render the render targets, they are sorted so that more deeply nested ones are rendered first. #ifdef LYSHINE_ATOM_TODO // keeping this code for reference for future phase (render targets) diff --git a/Gems/LyShine/Code/Source/RenderGraph.h b/Gems/LyShine/Code/Source/RenderGraph.h index 2f1586e857..355616a29a 100644 --- a/Gems/LyShine/Code/Source/RenderGraph.h +++ b/Gems/LyShine/Code/Source/RenderGraph.h @@ -22,12 +22,11 @@ #include #include +#include "UiRenderer.h" #ifndef _RELEASE #include "LyShineDebug.h" #endif -class UiRenderer; - namespace LyShine { enum RenderNodeType @@ -157,8 +156,8 @@ namespace LyShine #endif private: // functions - void SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState); - void SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, int priorBaseState); + void SetupBeforeRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState); + void SetupAfterRenderingMask(UiRenderer* uiRenderer, bool firstPass, UiRenderer::BaseState priorBaseState); private: // data AZStd::vector m_maskRenderNodes; //!< The render nodes used to render the mask shape diff --git a/Gems/LyShine/Code/Source/UiRenderer.cpp b/Gems/LyShine/Code/Source/UiRenderer.cpp index 6971aa3fc9..b1348d9ea7 100644 --- a/Gems/LyShine/Code/Source/UiRenderer.cpp +++ b/Gems/LyShine/Code/Source/UiRenderer.cpp @@ -20,7 +20,6 @@ #include #include #include -#include // LYSHINE_ATOM_TODO - remove when GS_DEPTHFUNC_LEQUAL reference is removed with LyShine render target Atom conversion #include #include @@ -33,9 +32,7 @@ //////////////////////////////////////////////////////////////////////////////////////////////////// UiRenderer::UiRenderer(AZ::RPI::ViewportContextPtr viewportContext) - : m_baseState(GS_DEPTHFUNC_LEQUAL) - , m_stencilRef(0) - , m_viewportContext(viewportContext) + : m_viewportContext(viewportContext) { // Use bootstrap scene event to indicate when the RPI has fully // initialized with all assets loaded and is ready to be used @@ -127,6 +124,8 @@ void UiRenderer::CreateDynamicDrawContext(AZ::RPI::ScenePtr scene, AZ::Data::Ins { "TEXCOORD", AZ::RHI::Format::R32G32_FLOAT }, { "BLENDINDICES", AZ::RHI::Format::R16G16_UINT } } ); + m_dynamicDraw->AddDrawStateOptions(AZ::RPI::DynamicDrawContext::DrawStateOptions::StencilState + | AZ::RPI::DynamicDrawContext::DrawStateOptions::BlendMode); m_dynamicDraw->EndInit(); } @@ -170,25 +169,24 @@ void UiRenderer::CacheShaderData(const AZ::RHI::Ptr shaderOptionsDefault.push_back(AZ::RPI::ShaderOption(AZ::Name("o_srgbWrite"), AZ::Name("true"))); shaderOptionsDefault.push_back(AZ::RPI::ShaderOption(AZ::Name("o_modulate"), AZ::Name("Modulate::None"))); m_uiShaderData.m_shaderVariantDefault = dynamicDraw->UseShaderVariant(shaderOptionsDefault); + AZ::RPI::ShaderOptionList shaderOptionsAlphaTest; + shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_preMultiplyAlpha"), AZ::Name("false"))); + shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_alphaTest"), AZ::Name("true"))); + shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_srgbWrite"), AZ::Name("true"))); + shaderOptionsAlphaTest.push_back(AZ::RPI::ShaderOption(AZ::Name("o_modulate"), AZ::Name("Modulate::None"))); + m_uiShaderData.m_shaderVariantAlphaTest = dynamicDraw->UseShaderVariant(shaderOptionsAlphaTest); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiRenderer::BeginUiFrameRender() { -#ifdef LYSHINE_ATOM_TODO - m_renderer = gEnv->pRenderer; - - // we are rendering at the end of the frame, after tone mapping, so we should be writing sRGB values - m_renderer->SetSrgbWrite(true); - #ifndef _RELEASE if (m_debugTextureDataRecordLevel > 0) { m_texturesUsedInFrame.clear(); } #endif -#endif - + // Various platform drivers expect all texture slots used in the shader to be bound BindNullTexture(); } @@ -204,18 +202,10 @@ void UiRenderer::EndUiFrameRender() //////////////////////////////////////////////////////////////////////////////////////////////////// void UiRenderer::BeginCanvasRender() { -#ifdef LYSHINE_ATOM_TODO - m_baseState = GS_NODEPTHTEST; - m_stencilRef = 0; - // Set default starting state - IRenderer* renderer = gEnv->pRenderer; - - renderer->SetCullMode(R_CULL_DISABLE); - renderer->SetColorOp(eCO_MODULATE, eCO_MODULATE, DEF_TEXARG0, DEF_TEXARG0); - renderer->SetState(GS_BLSRC_SRCALPHA | GS_BLDST_ONEMINUSSRCALPHA | GS_NODEPTHTEST); -#endif + // Set base state + m_baseState.ResetToDefault(); } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -229,11 +219,13 @@ AZ::RHI::Ptr UiRenderer::GetDynamicDrawContext() return m_dynamicDraw; } +//////////////////////////////////////////////////////////////////////////////////////////////////// const UiRenderer::UiShaderData& UiRenderer::GetUiShaderData() { return m_uiShaderData; } +//////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Matrix4x4 UiRenderer::GetModelViewProjectionMatrix() { auto viewportContext = GetViewportContext(); @@ -253,6 +245,7 @@ AZ::Matrix4x4 UiRenderer::GetModelViewProjectionMatrix() return modelViewProjMat; } +//////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiRenderer::GetViewportSize() { auto viewportContext = GetViewportContext(); @@ -267,17 +260,30 @@ AZ::Vector2 UiRenderer::GetViewportSize() } //////////////////////////////////////////////////////////////////////////////////////////////////// -int UiRenderer::GetBaseState() +UiRenderer::BaseState UiRenderer::GetBaseState() { return m_baseState; } //////////////////////////////////////////////////////////////////////////////////////////////////// -void UiRenderer::SetBaseState(int state) +void UiRenderer::SetBaseState(BaseState state) { m_baseState = state; } +//////////////////////////////////////////////////////////////////////////////////////////////////// +AZ::RPI::ShaderVariantId UiRenderer::GetCurrentShaderVariant() +{ + AZ::RPI::ShaderVariantId variantId = m_uiShaderData.m_shaderVariantDefault; + + if (m_baseState.m_useAlphaTest) + { + variantId = m_uiShaderData.m_shaderVariantAlphaTest; + } + + return variantId; +} + //////////////////////////////////////////////////////////////////////////////////////////////////// uint32 UiRenderer::GetStencilRef() { @@ -354,6 +360,7 @@ void UiRenderer::DebugDisplayTextureData(int recordingOption) { if (recordingOption > 0) { +#ifdef LYSHINE_ATOM_TODO // Convert debug to use Atom images // compute the total area of all the textures, also create a vector that we can sort by area AZStd::vector textures; int totalArea = 0; @@ -431,6 +438,7 @@ void UiRenderer::DebugDisplayTextureData(int recordingOption) texture->GetWidth(), texture->GetHeight(), texture->GetDataSize(), texture->GetFormatName(), texture->GetName()); WriteLine(buffer, white); } +#endif } } diff --git a/Gems/LyShine/Code/Source/UiRenderer.h b/Gems/LyShine/Code/Source/UiRenderer.h index 888c88586a..413e8c45cf 100644 --- a/Gems/LyShine/Code/Source/UiRenderer.h +++ b/Gems/LyShine/Code/Source/UiRenderer.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #ifndef _RELEASE @@ -38,6 +39,36 @@ public: // types AZ::RHI::ShaderInputConstantIndex m_isClampInputIndex; AZ::RPI::ShaderVariantId m_shaderVariantDefault; + AZ::RPI::ShaderVariantId m_shaderVariantAlphaTest; + }; + + // Base state + struct BaseState + { + BaseState() + { + ResetToDefault(); + } + + void ResetToDefault() + { + // Enable blend/color write + m_blendState.m_enable = true; + m_blendState.m_writeMask = 0xF; + m_blendState.m_blendSource = AZ::RHI::BlendFactor::AlphaSource; + m_blendState.m_blendDest = AZ::RHI::BlendFactor::AlphaSourceInverse; + m_blendState.m_blendOp = AZ::RHI::BlendOp::Add; + + // Disable stencil + m_stencilState = AZ::RHI::StencilState(); + m_stencilState.m_enable = 0; + + m_useAlphaTest = false; + } + + AZ::RHI::TargetBlendState m_blendState; + AZ::RHI::StencilState m_stencilState; + bool m_useAlphaTest = false; }; public: // member functions @@ -74,10 +105,13 @@ public: // member functions AZ::Vector2 GetViewportSize(); //! Get the current base state - int GetBaseState(); + BaseState GetBaseState(); //! Set the base state - void SetBaseState(int state); + void SetBaseState(BaseState state); + + //! Get the shader variant based on current render properties + AZ::RPI::ShaderVariantId GetCurrentShaderVariant(); //! Get the current stencil test reference value uint32 GetStencilRef(); @@ -126,8 +160,8 @@ protected: // attributes static constexpr char LogName[] = "UiRenderer"; - int m_baseState; - uint32 m_stencilRef; + BaseState m_baseState; + uint32 m_stencilRef = 0; UiShaderData m_uiShaderData; AZ::RHI::Ptr m_dynamicDraw; diff --git a/Gems/LyShineExamples/Assets/UI/Textures/LyShineExamples/CircleMask.tif b/Gems/LyShineExamples/Assets/UI/Textures/LyShineExamples/CircleMask.tif new file mode 100644 index 0000000000..fd29516609 --- /dev/null +++ b/Gems/LyShineExamples/Assets/UI/Textures/LyShineExamples/CircleMask.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8474b897fe02f70ed8d0e5c47cae8c816c832a7a5739b8c32317737fd275774f +size 1069752 diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index e4256a875c..c43d136cd8 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -179,7 +179,10 @@ namespace Multiplayer AZ::TickBus::Handler::BusConnect(); AzFramework::SessionNotificationBus::Handler::BusConnect(); m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(MPNetworkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); - m_consoleCommandHandler.Connect(AZ::Interface::Get()->GetConsoleCommandInvokedEvent()); + if (AZ::Interface::Get()) + { + m_consoleCommandHandler.Connect(AZ::Interface::Get()->GetConsoleCommandInvokedEvent()); + } AZ::Interface::Register(this); AZ::Interface::Register(this); @@ -191,6 +194,8 @@ namespace Multiplayer { AZ::Interface::Unregister(this); AZ::Interface::Unregister(this); + m_consoleCommandHandler.Disconnect(); + AZ::Interface::Get()->DestroyNetworkInterface(AZ::Name(MPNetworkInterfaceName)); AzFramework::SessionNotificationBus::Handler::BusDisconnect(); AZ::TickBus::Handler::BusDisconnect(); } @@ -199,19 +204,10 @@ namespace Multiplayer { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + m_pendingConnectionTickets.push(config.m_playerSessionId); AZStd::string hostname = config.m_dnsName.empty() ? config.m_ipAddress : config.m_dnsName; const IpAddress ipAddress(hostname.c_str(), config.m_port, m_networkInterface->GetType()); - ConnectionId connectionId = m_networkInterface->Connect(ipAddress); - - AzNetworking::IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId); - if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so - { - connection->SetUserData(new ClientToServerConnectionData(connection, *this, config.m_playerSessionId)); - } - else - { - reinterpret_cast(connection->GetUserData())->SetProviderTicket(config.m_playerSessionId); - } + m_networkInterface->Connect(ipAddress); return true; } @@ -584,15 +580,16 @@ namespace Multiplayer datum.m_isInvited = false; datum.m_agentType = MultiplayerAgentType::Client; + AZStd::string providerTicket; if (connection->GetConnectionRole() == ConnectionRole::Connector) { AZLOG_INFO("New outgoing connection to remote address: %s", connection->GetRemoteAddress().GetString().c_str()); - AZ::CVarFixedString providerTicket; - if (connection->GetUserData() != nullptr) + if (!m_pendingConnectionTickets.empty()) { - providerTicket = reinterpret_cast(connection->GetUserData())->GetProviderTicket(); + providerTicket = m_pendingConnectionTickets.front(); + m_pendingConnectionTickets.pop(); } - connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket)); + connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket.c_str())); } else { @@ -622,7 +619,11 @@ namespace Multiplayer { if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so { - connection->SetUserData(new ClientToServerConnectionData(connection, *this)); + connection->SetUserData(new ClientToServerConnectionData(connection, *this, providerTicket)); + } + else + { + reinterpret_cast(connection->GetUserData())->SetProviderTicket(providerTicket); } AZStd::unique_ptr window = AZStd::make_unique(); @@ -646,14 +647,10 @@ namespace Multiplayer AZStd::string reasonString = ToString(reason); AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str()); - if (connection->GetConnectionRole() == ConnectionRole::Acceptor) - { - // The authority is shutting down its connection - m_shutdownEvent.Signal(m_networkInterface); - } - else if (GetAgentType() == MultiplayerAgentType::Client && connection->GetConnectionRole() == ConnectionRole::Connector) + // The client is disconnecting + if (GetAgentType() == MultiplayerAgentType::Client) { - // The client is disconnecting + AZ_Assert(connection->GetConnectionRole() == ConnectionRole::Connector, "Client connection role should only ever be Connector"); m_clientDisconnectedEvent.Signal(); } @@ -669,7 +666,7 @@ namespace Multiplayer if (m_agentType == MultiplayerAgentType::DedicatedServer || m_agentType == MultiplayerAgentType::ClientServer) { if (AZ::Interface::Get() != nullptr && - connection->GetConnectionRole() == ConnectionRole::Connector) + connection->GetConnectionRole() == ConnectionRole::Acceptor) { AzFramework::PlayerConnectionConfig config; config.m_playerConnectionId = aznumeric_cast(connection->GetConnectionId()); @@ -680,12 +677,15 @@ namespace Multiplayer // Signal to session management when there are no remaining players in a dedicated server for potential cleanup // We avoid this for client server as the host itself is a user - if (m_agentType == MultiplayerAgentType::DedicatedServer && connection->GetConnectionRole() == ConnectionRole::Connector) + if (m_agentType == MultiplayerAgentType::DedicatedServer && connection->GetConnectionRole() == ConnectionRole::Acceptor) { - if (AZ::Interface::Get() != nullptr - && m_networkInterface->GetConnectionSet().GetConnectionCount() == 0) + if (m_networkInterface->GetConnectionSet().GetConnectionCount() == 0) { - AZ::Interface::Get()->HandleDestroySession(); + m_shutdownEvent.Signal(m_networkInterface); + if (AZ::Interface::Get() != nullptr) + { + AZ::Interface::Get()->HandleDestroySession(); + } } } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 0efef3ebe4..00313c6b65 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -146,6 +146,8 @@ namespace Multiplayer ConnectionAcquiredEvent m_connAcquiredEvent; ClientDisconnectedEvent m_clientDisconnectedEvent; + AZStd::queue m_pendingConnectionTickets; + AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId; diff --git a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp index 7b09b65de4..9579c84fc1 100644 --- a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp +++ b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp @@ -35,14 +35,16 @@ namespace UnitTest m_initHandler = Multiplayer::SessionInitEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestInitEvent(value); }); m_mpComponent->AddSessionInitHandler(m_initHandler); - m_shutdownHandler = Multiplayer::SessionInitEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestShutdownEvent(value); }); + m_shutdownHandler = Multiplayer::SessionShutdownEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestShutdownEvent(value); }); m_mpComponent->AddSessionShutdownHandler(m_shutdownHandler); m_connAcquiredHandler = Multiplayer::ConnectionAcquiredEvent::Handler([this](Multiplayer::MultiplayerAgentDatum value) { TestConnectionAcquiredEvent(value); }); m_mpComponent->AddConnectionAcquiredHandler(m_connAcquiredHandler); + m_mpComponent->Activate(); } void TearDown() override { + m_mpComponent->Deactivate(); delete m_mpComponent; delete m_netComponent; AZ::NameDictionary::Destroy(); @@ -86,6 +88,7 @@ namespace UnitTest TEST_F(MultiplayerSystemTests, TestShutdownEvent) { + m_mpComponent->InitializeMultiplayer(Multiplayer::MultiplayerAgentType::DedicatedServer); IMultiplayerConnectionMock connMock1 = IMultiplayerConnectionMock(AzNetworking::ConnectionId(), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Acceptor); IMultiplayerConnectionMock connMock2 = IMultiplayerConnectionMock(AzNetworking::ConnectionId(), AzNetworking::IpAddress(), AzNetworking::ConnectionRole::Connector); m_mpComponent->OnDisconnect(&connMock1, AzNetworking::DisconnectReason::None, AzNetworking::TerminationEndpoint::Local); diff --git a/Gems/NvCloth/Code/Source/Components/EditorClothComponent.cpp b/Gems/NvCloth/Code/Source/Components/EditorClothComponent.cpp index 7da677a7eb..7eb07f46d7 100644 --- a/Gems/NvCloth/Code/Source/Components/EditorClothComponent.cpp +++ b/Gems/NvCloth/Code/Source/Components/EditorClothComponent.cpp @@ -52,7 +52,7 @@ namespace NvCloth ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Cloth.svg") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Cloth.svg") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) - ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.aws.amazon.com/lumberyard/latest/userguide/component-cloth.html") + ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.o3de.org/docs/user-guide/components/reference/cloth/") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->UIElement(AZ::Edit::UIHandlers::CheckBox, "Simulate in editor", diff --git a/Gems/PhysX/Code/CMakeLists.txt b/Gems/PhysX/Code/CMakeLists.txt index d281270ce4..caeaf884bf 100644 --- a/Gems/PhysX/Code/CMakeLists.txt +++ b/Gems/PhysX/Code/CMakeLists.txt @@ -156,6 +156,8 @@ endif() ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + + ly_add_target( NAME PhysX.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -174,6 +176,15 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) RUNTIME_DEPENDENCIES Gem::LmbrCentral ) + + if(PAL_TRAIT_JOINTS_TYPED_TEST_CASE) + ly_add_source_properties( + SOURCES Tests/PhysXJointsTest.cpp + PROPERTY COMPILE_DEFINITIONS + VALUES ENABLE_JOINTS_TYPED_TEST_CASE + ) + endif() + ly_add_googletest( NAME Gem::PhysX.Tests ) diff --git a/Gems/PhysX/Code/Editor/ColliderComponentMode.cpp b/Gems/PhysX/Code/Editor/ColliderComponentMode.cpp index 7caa497344..0957985623 100644 --- a/Gems/PhysX/Code/Editor/ColliderComponentMode.cpp +++ b/Gems/PhysX/Code/Editor/ColliderComponentMode.cpp @@ -223,7 +223,7 @@ namespace PhysX AzToolsFramework::ViewportUi::ViewportUiRequestBus::EventResult( buttonId, AzToolsFramework::ViewportUi::DefaultViewportId, &AzToolsFramework::ViewportUi::ViewportUiRequestBus::Events::CreateClusterButton, clusterId, - AZStd::string::format("Icons/PhysX/%s.svg", iconName)); + AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s.svg", iconName)); return buttonId; } diff --git a/Gems/PhysX/Code/Editor/EditorJointConfiguration.cpp b/Gems/PhysX/Code/Editor/EditorJointConfiguration.cpp index 7b4ec15880..0ceec25918 100644 --- a/Gems/PhysX/Code/Editor/EditorJointConfiguration.cpp +++ b/Gems/PhysX/Code/Editor/EditorJointConfiguration.cpp @@ -144,11 +144,12 @@ namespace PhysX { return m_standardLimitConfig.m_isLimited; } - GenericJointLimitsConfiguration EditorJointLimitPairConfig::ToGameTimeConfig() const + + JointLimitProperties EditorJointLimitPairConfig::ToGameTimeConfig() const { - return GenericJointLimitsConfiguration(m_standardLimitConfig.m_damping - , m_standardLimitConfig.m_isLimited + return JointLimitProperties(m_standardLimitConfig.m_isLimited , m_standardLimitConfig.m_isSoftLimit + , m_standardLimitConfig.m_damping , m_limitPositive, m_limitNegative , m_standardLimitConfig.m_stiffness , m_standardLimitConfig.m_tolerance); @@ -193,11 +194,11 @@ namespace PhysX return m_standardLimitConfig.m_isLimited; } - GenericJointLimitsConfiguration EditorJointLimitConeConfig::ToGameTimeConfig() const + JointLimitProperties EditorJointLimitConeConfig::ToGameTimeConfig() const { - return GenericJointLimitsConfiguration(m_standardLimitConfig.m_damping - , m_standardLimitConfig.m_isLimited + return JointLimitProperties(m_standardLimitConfig.m_isLimited , m_standardLimitConfig.m_isSoftLimit + , m_standardLimitConfig.m_damping , m_limitY , m_limitZ , m_standardLimitConfig.m_stiffness @@ -275,27 +276,31 @@ namespace PhysX , AzToolsFramework::Refresh_AttributesAndValues); } - GenericJointConfiguration EditorJointConfig::ToGameTimeConfig() const + JointGenericProperties EditorJointConfig::ToGenericProperties() const { - GenericJointConfiguration::GenericJointFlag flags = GenericJointConfiguration::GenericJointFlag::None; + JointGenericProperties::GenericJointFlag flags = JointGenericProperties::GenericJointFlag::None; if (m_breakable) { - flags |= GenericJointConfiguration::GenericJointFlag::Breakable; + flags |= JointGenericProperties::GenericJointFlag::Breakable; } if (m_selfCollide) { - flags |= GenericJointConfiguration::GenericJointFlag::SelfCollide; + flags |= JointGenericProperties::GenericJointFlag::SelfCollide; } + return JointGenericProperties(flags, m_forceMax, m_torqueMax); + } + + JointComponentConfiguration EditorJointConfig::ToGameTimeConfig() const + { AZ::Vector3 localRotation(m_localRotation); - return GenericJointConfiguration(m_forceMax - , m_torqueMax - , AZ::Transform::CreateFromQuaternionAndTranslation(AZ::Quaternion::CreateFromEulerAnglesDegrees(localRotation), - m_localPosition) - , m_leadEntity - , m_followerEntity - , flags); + return JointComponentConfiguration( + AZ::Transform::CreateFromQuaternionAndTranslation( + AZ::Quaternion::CreateFromEulerAnglesDegrees(localRotation), + m_localPosition), + m_leadEntity, + m_followerEntity); } bool EditorJointConfig::IsInComponentMode() const diff --git a/Gems/PhysX/Code/Editor/EditorJointConfiguration.h b/Gems/PhysX/Code/Editor/EditorJointConfiguration.h index a7146124f0..aa0062b3ac 100644 --- a/Gems/PhysX/Code/Editor/EditorJointConfiguration.h +++ b/Gems/PhysX/Code/Editor/EditorJointConfiguration.h @@ -15,7 +15,7 @@ #include #include -#include +#include namespace PhysX { @@ -68,7 +68,7 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); bool IsLimited() const; - GenericJointLimitsConfiguration ToGameTimeConfig() const; + JointLimitProperties ToGameTimeConfig() const; EditorJointLimitConfig m_standardLimitConfig; float m_limitPositive = 45.0f; @@ -87,7 +87,7 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); bool IsLimited() const; - GenericJointLimitsConfiguration ToGameTimeConfig() const; + JointLimitProperties ToGameTimeConfig() const; EditorJointLimitConfig m_standardLimitConfig; float m_limitY = 45.0f; @@ -105,7 +105,8 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); void SetLeadEntityId(AZ::EntityId leadEntityId); - GenericJointConfiguration ToGameTimeConfig() const; + JointGenericProperties ToGenericProperties() const; + JointComponentConfiguration ToGameTimeConfig() const; bool m_breakable = false; bool m_displayJointSetup = false; diff --git a/Gems/PhysX/Code/Include/PhysX/Joint/Configuration/PhysXJointConfiguration.h b/Gems/PhysX/Code/Include/PhysX/Joint/Configuration/PhysXJointConfiguration.h new file mode 100644 index 0000000000..873a939192 --- /dev/null +++ b/Gems/PhysX/Code/Include/PhysX/Joint/Configuration/PhysXJointConfiguration.h @@ -0,0 +1,107 @@ +/* +* 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 PhysX +{ + struct D6JointLimitConfiguration + : public AzPhysics::JointConfiguration + { + AZ_CLASS_ALLOCATOR(D6JointLimitConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(D6JointLimitConfiguration, "{88E067B4-21E8-4FFA-9142-6C52605B704C}", AzPhysics::JointConfiguration); + static void Reflect(AZ::ReflectContext* context); + + float m_swingLimitY = 45.0f; ///< Maximum angle in degrees from the Y axis of the joint frame. + float m_swingLimitZ = 45.0f; ///< Maximum angle in degrees from the Z axis of the joint frame. + float m_twistLimitLower = -45.0f; ///< Lower limit in degrees for rotation about the X axis of the joint frame. + float m_twistLimitUpper = 45.0f; ///< Upper limit in degrees for rotation about the X axis of the joint frame. + }; + + //! Properties that are common for several types of joints. + struct JointGenericProperties + { + enum class GenericJointFlag : AZ::u16 + { + None = 0, + Breakable = 1, + SelfCollide = 1 << 1 + }; + + AZ_CLASS_ALLOCATOR(JointGenericProperties, AZ::SystemAllocator, 0); + AZ_TYPE_INFO(JointGenericProperties, "{6CB15399-24F6-4F03-AAEF-1AE013B683E0}"); + static void Reflect(AZ::ReflectContext* context); + + JointGenericProperties() = default; + JointGenericProperties(GenericJointFlag flags, float forceMax, float torqueMax); + + bool IsFlagSet(GenericJointFlag flag) const; ///< Returns if a particular flag is set as a bool. + + /// Flags that indicates if joint is breakable, self-colliding, etc. + /// Converting joint between breakable/non-breakable at game time is allowed. + GenericJointFlag m_flags = GenericJointFlag::None; + float m_forceMax = 1.0f; ///< Max force joint can tolerate before breaking. + float m_torqueMax = 1.0f; ///< Max torque joint can tolerate before breaking. + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(PhysX::JointGenericProperties::GenericJointFlag) + + struct JointLimitProperties + { + AZ_CLASS_ALLOCATOR(JointLimitProperties, AZ::SystemAllocator, 0); + AZ_TYPE_INFO(JointLimitProperties, "{31F941CB-6699-48BB-B12D-61874B52B984}"); + static void Reflect(AZ::ReflectContext* context); + + JointLimitProperties() = default; + JointLimitProperties( + bool isLimited, bool isSoftLimit, + float damping, float limitFirst, float limitSecond, float stiffness, float tolerance); + + bool m_isLimited = true; ///< Specifies if limits are applied to the joint constraints. E.g. if the swing angles are limited. + bool m_isSoftLimit = false; ///< If limit is soft, spring and damping are used, otherwise tolerance is used. Converting between soft/hard limit at game time is allowed. + float m_damping = 20.0f; ///< The damping strength of the drive, the force proportional to the velocity error. Used if limit is soft. + float m_limitFirst = 45.0f; ///< Positive angle limit in the case of twist angle limits, Y-axis swing limit in the case of cone limits. + float m_limitSecond = 45.0f; ///< Negative angle limit in the case of twist angle limits, Z-axis swing limit in the case of cone limits. + float m_stiffness = 100.0f; ///< The spring strength of the drive, the force proportional to the position error. Used if limit is soft. + float m_tolerance = 0.1f; ///< Distance from the joint at which limits becomes enforced. Used if limit is hard. + }; + + struct FixedJointConfiguration : public AzPhysics::JointConfiguration + { + AZ_CLASS_ALLOCATOR(FixedJointConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(FixedJointConfiguration, "{9BCB368B-8D71-4928-B231-0225907E3BD9}", AzPhysics::JointConfiguration); + static void Reflect(AZ::ReflectContext* context); + + JointGenericProperties m_genericProperties; + }; + + struct BallJointConfiguration : public AzPhysics::JointConfiguration + { + AZ_CLASS_ALLOCATOR(BallJointConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(BallJointConfiguration, "{C2DE2479-B752-469D-BE05-900CD2CD8481}", AzPhysics::JointConfiguration); + static void Reflect(AZ::ReflectContext* context); + + JointGenericProperties m_genericProperties; + JointLimitProperties m_limitProperties; + }; + + struct HingeJointConfiguration : public AzPhysics::JointConfiguration + { + AZ_CLASS_ALLOCATOR(HingeJointConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(HingeJointConfiguration, "{FB04198E-0BA5-45C2-8343-66DA28ED45EA}", AzPhysics::JointConfiguration); + static void Reflect(AZ::ReflectContext* context); + + JointGenericProperties m_genericProperties; + JointLimitProperties m_limitProperties; + }; +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/BallJointComponent.cpp b/Gems/PhysX/Code/Source/BallJointComponent.cpp index b37efd5a33..0f494c6714 100644 --- a/Gems/PhysX/Code/Source/BallJointComponent.cpp +++ b/Gems/PhysX/Code/Source/BallJointComponent.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include @@ -33,15 +35,17 @@ namespace PhysX } } - BallJointComponent::BallJointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& swingLimit) - : JointComponent(config, swingLimit) + BallJointComponent::BallJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties) + : JointComponent(configuration, genericProperties, limitProperties) { } void BallJointComponent::InitNativeJoint() { - if (m_joint) + if (m_jointHandle != AzPhysics::InvalidJointHandle) { return; } @@ -52,50 +56,24 @@ namespace PhysX { return; } - PHYSX_SCENE_READ_LOCK(leadFollowerInfo.m_followerActor->getScene()); - m_joint = AZStd::make_shared(physx::PxSphericalJointCreate( - PxGetPhysics(), - leadFollowerInfo.m_leadActor, - leadFollowerInfo.m_leadLocal, - leadFollowerInfo.m_followerActor, - leadFollowerInfo.m_followerLocal), - leadFollowerInfo.m_leadBody, - leadFollowerInfo.m_followerBody); - InitSwingLimits(); - } - - void BallJointComponent::InitSwingLimits() - { - if (!m_joint) - { - return; - } - - physx::PxSphericalJoint* ballJointNative = static_cast(m_joint->GetNativePointer()); - if (!ballJointNative) - { - return; - } + BallJointConfiguration configuration; + configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); + configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); + configuration.m_childLocalPosition = leadFollowerInfo.m_followerLocal.GetTranslation(); + configuration.m_childLocalRotation = leadFollowerInfo.m_followerLocal.GetRotation(); - if (!m_limits.m_isLimited) - { - ballJointNative->setSphericalJointFlag(physx::PxSphericalJointFlag::eLIMIT_ENABLED, false); - return; - } + configuration.m_genericProperties = m_genericProperties; + configuration.m_limitProperties = m_limits; - // Hard limit uses a tolerance value (distance to limit at which limit becomes active). - // Soft limit allows angle to exceed limit but springs back with configurable spring stiffness and damping. - physx::PxJointLimitCone swingLimit(AZ::DegToRad(m_limits.m_limitFirst) - , AZ::DegToRad(m_limits.m_limitSecond) - , m_limits.m_tolerance); - if (m_limits.m_isSoftLimit) + if (auto* sceneInterface = AZ::Interface::Get()) { - swingLimit.stiffness = m_limits.m_stiffness; - swingLimit.damping = m_limits.m_damping; + m_jointHandle = sceneInterface->AddJoint( + leadFollowerInfo.m_followerBody->m_sceneOwner, + &configuration, + leadFollowerInfo.m_leadBody->m_bodyHandle, + leadFollowerInfo.m_followerBody->m_bodyHandle); + m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; } - - ballJointNative->setLimitCone(swingLimit); - ballJointNative->setSphericalJointFlag(physx::PxSphericalJointFlag::eLIMIT_ENABLED, true); } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/BallJointComponent.h b/Gems/PhysX/Code/Source/BallJointComponent.h index 4ac489a5fb..9741193e81 100644 --- a/Gems/PhysX/Code/Source/BallJointComponent.h +++ b/Gems/PhysX/Code/Source/BallJointComponent.h @@ -25,15 +25,14 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); BallJointComponent() = default; - BallJointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& swingLimit); + BallJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties); ~BallJointComponent() = default; protected: // JointComponent void InitNativeJoint() override; - - private: - void InitSwingLimits(); }; } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/EditorBallJointComponent.cpp b/Gems/PhysX/Code/Source/EditorBallJointComponent.cpp index deb1d0d231..37203330b2 100644 --- a/Gems/PhysX/Code/Source/EditorBallJointComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorBallJointComponent.cpp @@ -99,6 +99,7 @@ namespace PhysX m_config.m_followerEntity = GetEntityId(); // joint is always in the same entity as the follower body. gameEntity->CreateComponent( m_config.ToGameTimeConfig(), + m_config.ToGenericProperties(), m_swingLimit.ToGameTimeConfig()); } diff --git a/Gems/PhysX/Code/Source/EditorBallJointComponent.h b/Gems/PhysX/Code/Source/EditorBallJointComponent.h index 44f96b3fa8..e06c026a97 100644 --- a/Gems/PhysX/Code/Source/EditorBallJointComponent.h +++ b/Gems/PhysX/Code/Source/EditorBallJointComponent.h @@ -13,7 +13,6 @@ #pragma once -#include #include #include #include diff --git a/Gems/PhysX/Code/Source/EditorColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorColliderComponent.cpp index 0671e3e77e..3cb931d148 100644 --- a/Gems/PhysX/Code/Source/EditorColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorColliderComponent.cpp @@ -194,7 +194,7 @@ namespace PhysX ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXCollider.svg") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/PhysXCollider.svg") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) - ->Attribute(AZ::Edit::Attributes::HelpPageURL, "http://docs.aws.amazon.com/console/lumberyard/component/physx/collider") + ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.o3de.org/docs/user-guide/components/reference/physx-collider/") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &EditorColliderComponent::m_configuration, "Collider Configuration", "Configuration of the collider") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) diff --git a/Gems/PhysX/Code/Source/EditorFixedJointComponent.cpp b/Gems/PhysX/Code/Source/EditorFixedJointComponent.cpp index 1645d8c932..8af78b005f 100644 --- a/Gems/PhysX/Code/Source/EditorFixedJointComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorFixedJointComponent.cpp @@ -94,6 +94,6 @@ namespace PhysX void EditorFixedJointComponent::BuildGameEntity(AZ::Entity* gameEntity) { m_config.m_followerEntity = GetEntityId(); // joint is always in the same entity as the follower body. - gameEntity->CreateComponent(m_config.ToGameTimeConfig()); + gameEntity->CreateComponent(m_config.ToGameTimeConfig(), m_config.ToGenericProperties()); } } diff --git a/Gems/PhysX/Code/Source/EditorFixedJointComponent.h b/Gems/PhysX/Code/Source/EditorFixedJointComponent.h index 8642f83472..b9275186e9 100644 --- a/Gems/PhysX/Code/Source/EditorFixedJointComponent.h +++ b/Gems/PhysX/Code/Source/EditorFixedJointComponent.h @@ -13,7 +13,6 @@ #pragma once -#include #include #include #include diff --git a/Gems/PhysX/Code/Source/EditorForceRegionComponent.cpp b/Gems/PhysX/Code/Source/EditorForceRegionComponent.cpp index 770200cda0..b783ea3185 100644 --- a/Gems/PhysX/Code/Source/EditorForceRegionComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorForceRegionComponent.cpp @@ -176,7 +176,7 @@ namespace PhysX ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/ForceRegion.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/ForceRegion.png") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) - ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.aws.amazon.com/console/lumberyard/physx/force-region") + ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.o3de.org/docs/user-guide/components/reference/physx-force-region/") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->Attribute(AZ::Edit::Attributes::RequiredService, AZ_CRC("PhysXTriggerService", 0x3a117d7b)) ->DataElement(AZ::Edit::UIHandlers::Default, &EditorForceRegionComponent::m_visibleInEditor, "Visible", "Always show the component in viewport") diff --git a/Gems/PhysX/Code/Source/EditorHingeJointComponent.cpp b/Gems/PhysX/Code/Source/EditorHingeJointComponent.cpp index 6d136e898b..3f263c11d4 100644 --- a/Gems/PhysX/Code/Source/EditorHingeJointComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorHingeJointComponent.cpp @@ -99,6 +99,7 @@ namespace PhysX m_config.m_followerEntity = GetEntityId(); // joint is always in the same entity as the follower body. gameEntity->CreateComponent( m_config.ToGameTimeConfig(), + m_config.ToGenericProperties(), m_angularLimit.ToGameTimeConfig()); } diff --git a/Gems/PhysX/Code/Source/EditorHingeJointComponent.h b/Gems/PhysX/Code/Source/EditorHingeJointComponent.h index ef555d145e..abb6baf3ea 100644 --- a/Gems/PhysX/Code/Source/EditorHingeJointComponent.h +++ b/Gems/PhysX/Code/Source/EditorHingeJointComponent.h @@ -13,7 +13,6 @@ #pragma once -#include #include #include #include diff --git a/Gems/PhysX/Code/Source/EditorJointComponent.h b/Gems/PhysX/Code/Source/EditorJointComponent.h index 10f2a6fc72..9082f6bc33 100644 --- a/Gems/PhysX/Code/Source/EditorJointComponent.h +++ b/Gems/PhysX/Code/Source/EditorJointComponent.h @@ -13,7 +13,6 @@ #pragma once -#include #include #include diff --git a/Gems/PhysX/Code/Source/EditorRigidBodyComponent.cpp b/Gems/PhysX/Code/Source/EditorRigidBodyComponent.cpp index f68c4d17d8..588b1d918f 100644 --- a/Gems/PhysX/Code/Source/EditorRigidBodyComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorRigidBodyComponent.cpp @@ -309,7 +309,7 @@ namespace PhysX ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/PhysXRigidBody.svg") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c)) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.aws.amazon.com/console/lumberyard/components/physx/rigid-body") + ->Attribute(AZ::Edit::Attributes::HelpPageURL, "https://docs.o3de.org/docs/user-guide/components/reference/physx-rigid-body-physics/") ->DataElement(0, &EditorRigidBodyComponent::m_config, "Configuration", "Configuration for rigid body physics.") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorRigidBodyComponent::CreateEditorWorldRigidBody) diff --git a/Gems/PhysX/Code/Source/FixedJointComponent.cpp b/Gems/PhysX/Code/Source/FixedJointComponent.cpp index 8170477aeb..13a42ffbac 100644 --- a/Gems/PhysX/Code/Source/FixedJointComponent.cpp +++ b/Gems/PhysX/Code/Source/FixedJointComponent.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include @@ -33,20 +36,24 @@ namespace PhysX } } - FixedJointComponent::FixedJointComponent(const GenericJointConfiguration& config) - : JointComponent(config) + FixedJointComponent::FixedJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties) + : JointComponent(configuration, genericProperties) { } - FixedJointComponent::FixedJointComponent(const GenericJointConfiguration& config, - const GenericJointLimitsConfiguration& limitConfig) - : JointComponent(config, limitConfig) + FixedJointComponent::FixedJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties) + : JointComponent(configuration, genericProperties, limitProperties) { } void FixedJointComponent::InitNativeJoint() { - if (m_joint) + if (m_jointHandle != AzPhysics::InvalidJointHandle) { return; } @@ -57,14 +64,23 @@ namespace PhysX { return; } - PHYSX_SCENE_READ_LOCK(leadFollowerInfo.m_followerActor->getScene()); - m_joint = AZStd::make_shared(physx::PxFixedJointCreate( - PxGetPhysics(), - leadFollowerInfo.m_leadActor, - leadFollowerInfo.m_leadLocal, - leadFollowerInfo.m_followerActor, - leadFollowerInfo.m_followerLocal), - leadFollowerInfo.m_leadBody, - leadFollowerInfo.m_followerBody); + + FixedJointConfiguration configuration; + configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); + configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); + configuration.m_childLocalPosition = leadFollowerInfo.m_followerLocal.GetTranslation(); + configuration.m_childLocalRotation = leadFollowerInfo.m_followerLocal.GetRotation(); + + configuration.m_genericProperties = m_genericProperties; + + if (auto* sceneInterface = AZ::Interface::Get()) + { + m_jointHandle = sceneInterface->AddJoint( + leadFollowerInfo.m_followerBody->m_sceneOwner, + &configuration, + leadFollowerInfo.m_leadBody->m_bodyHandle, + leadFollowerInfo.m_followerBody->m_bodyHandle); + m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; + } } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/FixedJointComponent.h b/Gems/PhysX/Code/Source/FixedJointComponent.h index 0f64eea2bb..1fd7d4d999 100644 --- a/Gems/PhysX/Code/Source/FixedJointComponent.h +++ b/Gems/PhysX/Code/Source/FixedJointComponent.h @@ -25,9 +25,13 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); FixedJointComponent() = default; - explicit FixedJointComponent(const GenericJointConfiguration& config); - FixedJointComponent(const GenericJointConfiguration& config, - const GenericJointLimitsConfiguration& limitConfig); + FixedJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties); + FixedJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties); ~FixedJointComponent() = default; protected: diff --git a/Gems/PhysX/Code/Source/HingeJointComponent.cpp b/Gems/PhysX/Code/Source/HingeJointComponent.cpp index 7b0702559c..41d948bd5d 100644 --- a/Gems/PhysX/Code/Source/HingeJointComponent.cpp +++ b/Gems/PhysX/Code/Source/HingeJointComponent.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include @@ -34,67 +36,45 @@ namespace PhysX } } - HingeJointComponent::HingeJointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& angularLimitConfig) - : JointComponent(config, angularLimitConfig) + HingeJointComponent::HingeJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties) + : JointComponent(configuration, genericProperties, limitProperties) { } void HingeJointComponent::InitNativeJoint() { - if (m_joint) + if (m_jointHandle != AzPhysics::InvalidJointHandle) { return; } JointComponent::LeadFollowerInfo leadFollowerInfo; ObtainLeadFollowerInfo(leadFollowerInfo); - if (!leadFollowerInfo.m_followerActor) + if (leadFollowerInfo.m_followerActor == nullptr || + leadFollowerInfo.m_leadBody == nullptr || + leadFollowerInfo.m_followerBody == nullptr) { return; } - PHYSX_SCENE_READ_LOCK(leadFollowerInfo.m_followerActor->getScene()); - m_joint = AZStd::make_shared(physx::PxRevoluteJointCreate( - PxGetPhysics(), - leadFollowerInfo.m_leadActor, - leadFollowerInfo.m_leadLocal, - leadFollowerInfo.m_followerActor, - leadFollowerInfo.m_followerLocal), - leadFollowerInfo.m_leadBody, - leadFollowerInfo.m_followerBody); - InitAngularLimits(); - } - - void HingeJointComponent::InitAngularLimits() - { - if (!m_joint) - { - return; - } - - physx::PxRevoluteJoint* revoluteJointNative = static_cast(m_joint->GetNativePointer()); - if (!revoluteJointNative) - { - return; - } + HingeJointConfiguration configuration; + configuration.m_parentLocalPosition = leadFollowerInfo.m_leadLocal.GetTranslation(); + configuration.m_parentLocalRotation = leadFollowerInfo.m_leadLocal.GetRotation(); + configuration.m_childLocalPosition = leadFollowerInfo.m_followerLocal.GetTranslation(); + configuration.m_childLocalRotation = leadFollowerInfo.m_followerLocal.GetRotation(); - if (!m_limits.m_isLimited) - { - revoluteJointNative->setRevoluteJointFlag(physx::PxRevoluteJointFlag::eLIMIT_ENABLED, false); - return; - } + configuration.m_genericProperties = m_genericProperties; + configuration.m_limitProperties = m_limits; - physx::PxJointAngularLimitPair limitPair(AZ::DegToRad(m_limits.m_limitSecond) - , AZ::DegToRad(m_limits.m_limitFirst) - , m_limits.m_tolerance); - if (m_limits.m_isSoftLimit) + if (auto* sceneInterface = AZ::Interface::Get()) { - limitPair.stiffness = m_limits.m_stiffness; - limitPair.damping = m_limits.m_damping; + m_jointHandle = sceneInterface->AddJoint( + leadFollowerInfo.m_followerBody->m_sceneOwner, &configuration, leadFollowerInfo.m_leadBody->m_bodyHandle, + leadFollowerInfo.m_followerBody->m_bodyHandle); + m_jointSceneOwner = leadFollowerInfo.m_followerBody->m_sceneOwner; } - - revoluteJointNative->setLimit(limitPair); - revoluteJointNative->setRevoluteJointFlag(physx::PxRevoluteJointFlag::eLIMIT_ENABLED, true); } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HingeJointComponent.h b/Gems/PhysX/Code/Source/HingeJointComponent.h index 357fe61b1a..8bc11a0051 100644 --- a/Gems/PhysX/Code/Source/HingeJointComponent.h +++ b/Gems/PhysX/Code/Source/HingeJointComponent.h @@ -25,15 +25,14 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); HingeJointComponent() = default; - explicit HingeJointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& angularLimit); + HingeJointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties); ~HingeJointComponent() = default; protected: // JointComponent void InitNativeJoint() override; - - private: - void InitAngularLimits(); }; } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint.cpp b/Gems/PhysX/Code/Source/Joint.cpp deleted file mode 100644 index 80575607c4..0000000000 --- a/Gems/PhysX/Code/Source/Joint.cpp +++ /dev/null @@ -1,646 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#include -#include -#include -#include -#include -#include -#include - -namespace PhysX -{ - namespace JointConstants - { - // Setting swing limits to very small values can cause extreme stability problems, so clamp above a small - // threshold. - static const float MinSwingLimitDegrees = 1.0f; - } // namespace JointConstants - - AzPhysics::SimulatedBody* Joint::GetParentBody() const - { - return m_parentBody; - } - - AzPhysics::SimulatedBody* Joint::GetChildBody() const - { - return m_childBody; - } - - bool IsAtLeastOneDynamic(AzPhysics::SimulatedBody* body0, - AzPhysics::SimulatedBody* body1) - { - for (const AzPhysics::SimulatedBody* body : { body0, body1 }) - { - if (body) - { - if (body->GetNativeType() == NativeTypeIdentifiers::RigidBody || - body->GetNativeType() == NativeTypeIdentifiers::ArticulationLink) - { - return true; - } - } - } - return false; - } - - physx::PxRigidActor* GetPxRigidActor(AzPhysics::SimulatedBody* worldBody) - { - if (worldBody && static_cast(worldBody->GetNativePointer())->is()) - { - return static_cast(worldBody->GetNativePointer()); - } - - return nullptr; - } - - void releasePxJoint(physx::PxJoint* joint) - { - PHYSX_SCENE_WRITE_LOCK(joint->getScene()); - joint->userData = nullptr; - joint->release(); - } - - Joint::Joint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody) - : m_parentBody(parentBody) - , m_childBody(childBody) - { - m_pxJoint = PxJointUniquePtr(pxJoint, releasePxJoint); - } - - bool Joint::SetPxActors() - { - physx::PxRigidActor* parentActor = GetPxRigidActor(m_parentBody); - physx::PxRigidActor* childActor = GetPxRigidActor(m_childBody); - if (!parentActor && !childActor) - { - AZ_Error("PhysX Joint", false, "Invalid PhysX actors in joint - at least one must be a PxRigidActor."); - return false; - } - - m_pxJoint->setActors(parentActor, childActor); - return true; - } - - void Joint::SetParentBody(AzPhysics::SimulatedBody* parentBody) - { - if (IsAtLeastOneDynamic(parentBody, m_childBody)) - { - m_parentBody = parentBody; - SetPxActors(); - } - else - { - AZ_Warning("PhysX Joint", false, "Call to SetParentBody would result in invalid joint - at least one " - "body in a joint must be dynamic."); - } - } - - void Joint::SetChildBody(AzPhysics::SimulatedBody* childBody) - { - if (IsAtLeastOneDynamic(m_parentBody, childBody)) - { - m_childBody = childBody; - SetPxActors(); - } - else - { - AZ_Warning("PhysX Joint", false, "Call to SetChildBody would result in invalid joint - at least one " - "body in a joint must be dynamic."); - } - } - - const AZStd::string& Joint::GetName() const - { - return m_name; - } - - void Joint::SetName(const AZStd::string& name) - { - m_name = name; - } - - void* Joint::GetNativePointer() - { - return m_pxJoint.get(); - } - - const AZ::Crc32 D6Joint::GetNativeType() const - { - return NativeTypeIdentifiers::D6Joint; - } - - void D6Joint::GenerateJointLimitVisualizationData( - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - [[maybe_unused]] AZStd::vector& vertexBufferOut, - [[maybe_unused]] AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) - { - const AZ::u32 angularSubdivisionsClamped = AZ::GetClamp(angularSubdivisions, 4u, 32u); - const AZ::u32 radialSubdivisionsClamped = AZ::GetClamp(radialSubdivisions, 1u, 4u); - - const physx::PxD6Joint* joint = static_cast(m_pxJoint.get()); - const AZ::Quaternion parentLocalRotation = PxMathConvert(joint->getLocalPose(physx::PxJointActorIndex::eACTOR0).q); - const AZ::Quaternion parentWorldRotation = m_parentBody ? m_parentBody->GetOrientation() : AZ::Quaternion::CreateIdentity(); - const AZ::Quaternion childLocalRotation = PxMathConvert(joint->getLocalPose(physx::PxJointActorIndex::eACTOR1).q); - const AZ::Quaternion childWorldRotation = m_childBody ? m_childBody->GetOrientation() : AZ::Quaternion::CreateIdentity(); - - const float swingAngleY = joint->getSwingYAngle(); - const float swingAngleZ = joint->getSwingZAngle(); - const float swingLimitY = joint->getSwingLimit().yAngle; - const float swingLimitZ = joint->getSwingLimit().zAngle; - const float twistAngle = joint->getTwist(); - const float twistLimitLower = joint->getTwistLimit().lower; - const float twistLimitUpper = joint->getTwistLimit().upper; - - JointUtils::AppendD6SwingConeToLineBuffer(parentLocalRotation, swingAngleY, swingAngleZ, swingLimitY, swingLimitZ, - scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); - JointUtils::AppendD6TwistArcToLineBuffer(parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, - scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); - JointUtils::AppendD6CurrentTwistToLineBuffer(parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, - scale, lineBufferOut, lineValidityBufferOut); - - // draw the X-axis of the child joint frame - // make the axis slightly longer than the radius of the twist arc so that it is easy to see - float axisLength = 1.25f * scale; - AZ::Vector3 childAxis = (parentWorldRotation.GetConjugate() * childWorldRotation * childLocalRotation).TransformVector( - AZ::Vector3::CreateAxisX(axisLength)); - lineBufferOut.push_back(AZ::Vector3::CreateZero()); - lineBufferOut.push_back(childAxis); - } - - const AZ::Crc32 FixedJoint::GetNativeType() const - { - return NativeTypeIdentifiers::FixedJoint; - } - - const AZ::Crc32 HingeJoint::GetNativeType() const - { - return NativeTypeIdentifiers::HingeJoint; - } - - const AZ::Crc32 BallJoint::GetNativeType() const - { - return NativeTypeIdentifiers::BallJoint; - } - - void GenericJointConfiguration::Reflect(AZ::ReflectContext* context) - { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(2, &VersionConverter) - ->Field("Follower Local Transform", &GenericJointConfiguration::m_localTransformFromFollower) - ->Field("Maximum Force", &GenericJointConfiguration::m_forceMax) - ->Field("Maximum Torque", &GenericJointConfiguration::m_torqueMax) - ->Field("Lead Entity", &GenericJointConfiguration::m_leadEntity) - ->Field("Follower Entity", &GenericJointConfiguration::m_followerEntity) - ->Field("Flags", &GenericJointConfiguration::m_flags) - ; - } - } - - bool GenericJointConfiguration::VersionConverter( - AZ::SerializeContext& context, - AZ::SerializeContext::DataElementNode& classElement) - { - if (classElement.GetVersion() <= 1) - { - // Convert bool breakable to GenericJointConfiguration::GenericJointFlag - const int breakableElementIndex = classElement.FindElement(AZ_CRC("Breakable", 0xb274ecd4)); - if (breakableElementIndex >= 0) - { - bool breakable = false; - AZ::SerializeContext::DataElementNode& breakableNode = classElement.GetSubElement(breakableElementIndex); - breakableNode.GetData(breakable); - if (!breakableNode.GetData(breakable)) - { - return false; - } - classElement.RemoveElement(breakableElementIndex); - GenericJointConfiguration::GenericJointFlag flags = breakable ? GenericJointConfiguration::GenericJointFlag::Breakable : GenericJointConfiguration::GenericJointFlag::None; - classElement.AddElementWithData(context, "Flags", flags); - } - } - - return true; - } - - GenericJointConfiguration::GenericJointConfiguration(float forceMax, - float torqueMax, - AZ::Transform localTransformFromFollower, - AZ::EntityId leadEntity, - AZ::EntityId followerEntity, - GenericJointFlag flags) - : m_forceMax(forceMax) - , m_torqueMax(torqueMax) - , m_localTransformFromFollower(localTransformFromFollower) - , m_leadEntity(leadEntity) - , m_followerEntity(followerEntity) - , m_flags(flags) - { - } - - bool GenericJointConfiguration::GetFlag(GenericJointFlag flag) - { - return static_cast(m_flags & flag); - } - - void GenericJointLimitsConfiguration::Reflect(AZ::ReflectContext* context) - { - if (auto* serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("First Limit", &GenericJointLimitsConfiguration::m_limitFirst) - ->Field("Second Limit", &GenericJointLimitsConfiguration::m_limitSecond) - ->Field("Tolerance", &GenericJointLimitsConfiguration::m_tolerance) - ->Field("Is Limited", &GenericJointLimitsConfiguration::m_isLimited) - ->Field("Is Soft Limit", &GenericJointLimitsConfiguration::m_isSoftLimit) - ->Field("Damping", &GenericJointLimitsConfiguration::m_damping) - ->Field("Spring", &GenericJointLimitsConfiguration::m_stiffness) - ; - } - } - - GenericJointLimitsConfiguration::GenericJointLimitsConfiguration(float damping - , bool isLimited - , bool isSoftLimit - , float limitFirst - , float limitSecond - , float stiffness - , float tolerance) - : m_damping(damping) - , m_isLimited(isLimited) - , m_isSoftLimit(isSoftLimit) - , m_limitFirst(limitFirst) - , m_limitSecond(limitSecond) - , m_stiffness(stiffness) - , m_tolerance(tolerance) - { - } - - AZStd::vector JointUtils::GetSupportedJointTypes() - { - return AZStd::vector - { - D6JointLimitConfiguration::RTTI_Type() - }; - } - - AZStd::shared_ptr JointUtils::CreateJointLimitConfiguration([[maybe_unused]] AZ::TypeId jointType) - { - return AZStd::make_shared(); - } - - AZStd::shared_ptr JointUtils::CreateJoint(const AZStd::shared_ptr& configuration, - AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody) - { - if (!configuration) - { - AZ_Warning("PhysX Joint", false, "CreateJoint failed - configuration was nullptr."); - return nullptr; - } - - if (auto d6Config = AZStd::rtti_pointer_cast(configuration)) - { - if (!IsAtLeastOneDynamic(parentBody, childBody)) - { - AZ_Warning("PhysX Joint", false, "CreateJoint failed - at least one body must be dynamic."); - return nullptr; - } - - physx::PxRigidActor* parentActor = GetPxRigidActor(parentBody); - physx::PxRigidActor* childActor = GetPxRigidActor(childBody); - - if (!parentActor && !childActor) - { - AZ_Warning("PhysX Joint", false, "CreateJoint failed - at least one body must be a PxRigidActor."); - return nullptr; - } - - const physx::PxTransform parentWorldTransform = parentActor ? parentActor->getGlobalPose() : physx::PxTransform(physx::PxIdentity); - const physx::PxTransform childWorldTransform = childActor ? childActor->getGlobalPose() : physx::PxTransform(physx::PxIdentity); - const physx::PxVec3 childOffset = childWorldTransform.p - parentWorldTransform.p; - physx::PxTransform parentLocalTransform(PxMathConvert(d6Config->m_parentLocalRotation).getNormalized()); - const physx::PxTransform childLocalTransform(PxMathConvert(d6Config->m_childLocalRotation).getNormalized()); - parentLocalTransform.p = parentWorldTransform.q.rotateInv(childOffset); - - physx::PxD6Joint* joint = PxD6JointCreate(PxGetPhysics(), parentActor, parentLocalTransform, - childActor, childLocalTransform); - - joint->setMotion(physx::PxD6Axis::eTWIST, physx::PxD6Motion::eLIMITED); - joint->setMotion(physx::PxD6Axis::eSWING1, physx::PxD6Motion::eLIMITED); - joint->setMotion(physx::PxD6Axis::eSWING2, physx::PxD6Motion::eLIMITED); - - AZ_Warning("PhysX Joint", - d6Config->m_swingLimitY >= JointConstants::MinSwingLimitDegrees && d6Config->m_swingLimitZ >= JointConstants::MinSwingLimitDegrees, - "Very small swing limit requested for joint between \"%s\" and \"%s\", increasing to %f degrees to improve stability", - parentActor ? parentActor->getName() : "world", childActor ? childActor->getName() : "world", - JointConstants::MinSwingLimitDegrees); - float swingLimitY = AZ::DegToRad(AZ::GetMax(JointConstants::MinSwingLimitDegrees, d6Config->m_swingLimitY)); - float swingLimitZ = AZ::DegToRad(AZ::GetMax(JointConstants::MinSwingLimitDegrees, d6Config->m_swingLimitZ)); - physx::PxJointLimitCone limitCone(swingLimitY, swingLimitZ); - joint->setSwingLimit(limitCone); - - float twistLower = AZ::DegToRad(AZStd::GetMin(d6Config->m_twistLimitLower, d6Config->m_twistLimitUpper)); - float twistUpper = AZ::DegToRad(AZStd::GetMax(d6Config->m_twistLimitLower, d6Config->m_twistLimitUpper)); - physx::PxJointAngularLimitPair twistLimitPair(twistLower, twistUpper); - joint->setTwistLimit(twistLimitPair); - - return AZStd::make_shared(joint, parentBody, childBody); - } - else - { - AZ_Warning("PhysX Joint", false, "Unrecognized joint limit configuration."); - return nullptr; - } - } - - D6JointState JointUtils::CalculateD6JointState( - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& parentLocalRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Quaternion& childLocalRotation) - { - D6JointState result; - - const AZ::Quaternion parentRotation = parentWorldRotation * parentLocalRotation; - const AZ::Quaternion childRotation = childWorldRotation * childLocalRotation; - const AZ::Quaternion relativeRotation = parentRotation.GetConjugate() * childRotation; - AZ::Quaternion twistQuat = AZ::IsClose(relativeRotation.GetX(), 0.0f, AZ::Constants::FloatEpsilon) - ? AZ::Quaternion::CreateIdentity() - : AZ::Quaternion(relativeRotation.GetX(), 0.0f, 0.0f, relativeRotation.GetW()).GetNormalized(); - AZ::Quaternion swingQuat = relativeRotation * twistQuat.GetConjugate(); - - // make sure the twist angle has the correct sign for the rotation - twistQuat *= AZ::GetSign(twistQuat.GetX()); - // make sure we get the shortest arcs for the swing degrees of freedom - swingQuat *= AZ::GetSign(swingQuat.GetW()); - // the PhysX swing limits work in terms of tan quarter angles - result.m_swingAngleY = 4.0f * atan2f(swingQuat.GetY(), 1.0f + swingQuat.GetW()); - result.m_swingAngleZ = 4.0f * atan2f(swingQuat.GetZ(), 1.0f + swingQuat.GetW()); - const float twistAngle = twistQuat.GetAngle(); - // GetAngle returns an angle in the range 0..2 pi, but the twist limits work in the range -pi..pi - const float wrappedTwistAngle = twistAngle > AZ::Constants::Pi ? twistAngle - AZ::Constants::TwoPi : twistAngle; - result.m_twistAngle = wrappedTwistAngle; - - return result; - } - - bool JointUtils::IsD6SwingValid( - float swingAngleY, - float swingAngleZ, - float swingLimitY, - float swingLimitZ) - { - const float epsilon = AZ::Constants::FloatEpsilon; - const float yFactor = tanf(0.25f * swingAngleY) / AZStd::GetMax(epsilon, tanf(0.25f * swingLimitY)); - const float zFactor = tanf(0.25f * swingAngleZ) / AZStd::GetMax(epsilon, tanf(0.25f * swingLimitZ)); - - return (yFactor * yFactor + zFactor * zFactor <= 1.0f + epsilon); - } - - void JointUtils::AppendD6SwingConeToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float swingAngleY, - float swingAngleZ, - float swingLimitY, - float swingLimitZ, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) - { - const AZ::u32 numLinesSwingCone = angularSubdivisions * (1u + radialSubdivisions); - lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesSwingCone); - lineValidityBufferOut.reserve(lineValidityBufferOut.size() + numLinesSwingCone); - - // the orientation quat for a radial line in the cone can be represented in terms of sin and cos half angles - // these expressions can be efficiently calculated using tan quarter angles as follows: - // writing t = tan(x / 4) - // sin(x / 2) = 2 * t / (1 + t * t) - // cos(x / 2) = (1 - t * t) / (1 + t * t) - const float tanQuarterSwingZ = tanf(0.25f * swingLimitZ); - const float tanQuarterSwingY = tanf(0.25f * swingLimitY); - - AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); - for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) - { - const float angle = AZ::Constants::TwoPi / angularSubdivisions * angularIndex; - // the axis about which to rotate the x-axis to get the radial vector for this segment of the cone - const AZ::Vector3 rotationAxis(0, -tanQuarterSwingY * sinf(angle), tanQuarterSwingZ * cosf(angle)); - const float normalizationFactor = rotationAxis.GetLengthSq(); - const AZ::Quaternion radialVectorRotation = 1.0f / (1.0f + normalizationFactor) * - AZ::Quaternion::CreateFromVector3AndValue(2.0f * rotationAxis, 1.0f - normalizationFactor); - const AZ::Vector3 radialVector = (parentLocalRotation * radialVectorRotation).TransformVector(AZ::Vector3::CreateAxisX(scale)); - - if (angularIndex > 0) - { - for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) - { - float radiusFraction = 1.0f / radialSubdivisions * radialIndex; - lineBufferOut.push_back(radiusFraction * radialVector); - lineBufferOut.push_back(radiusFraction * previousRadialVector); - } - } - - if (angularIndex < angularSubdivisions) - { - lineBufferOut.push_back(AZ::Vector3::CreateZero()); - lineBufferOut.push_back(radialVector); - } - - previousRadialVector = radialVector; - } - - const bool swingValid = IsD6SwingValid(swingAngleY, swingAngleZ, swingLimitY, swingLimitZ); - lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesSwingCone, swingValid); - } - - void JointUtils::AppendD6TwistArcToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float twistAngle, - float twistLimitLower, - float twistLimitUpper, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) - { - const AZ::u32 numLinesTwistArc = angularSubdivisions * (1u + radialSubdivisions) + 1u; - lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesTwistArc); - - AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); - const float twistRange = twistLimitUpper - twistLimitLower; - - for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) - { - const float angle = twistLimitLower + twistRange / angularSubdivisions * angularIndex; - const AZ::Vector3 radialVector = parentLocalRotation.TransformVector(scale * AZ::Vector3(0.0f, cosf(angle), sinf(angle))); - - if (angularIndex > 0) - { - for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) - { - const float radiusFraction = 1.0f / radialSubdivisions * radialIndex; - lineBufferOut.push_back(radiusFraction * radialVector); - lineBufferOut.push_back(radiusFraction * previousRadialVector); - } - } - - lineBufferOut.push_back(AZ::Vector3::CreateZero()); - lineBufferOut.push_back(radialVector); - - previousRadialVector = radialVector; - } - - const bool twistValid = (twistAngle >= twistLimitLower && twistAngle <= twistLimitUpper); - lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesTwistArc, twistValid); - } - - void JointUtils::AppendD6CurrentTwistToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float twistAngle, - [[maybe_unused]] float twistLimitLower, - [[maybe_unused]] float twistLimitUpper, - float scale, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut - ) - { - const AZ::Vector3 twistVector = parentLocalRotation.TransformVector(1.25f * scale * AZ::Vector3(0.0f, cosf(twistAngle), sinf(twistAngle))); - lineBufferOut.push_back(AZ::Vector3::CreateZero()); - lineBufferOut.push_back(twistVector); - lineValidityBufferOut.push_back(true); - } - - void JointUtils::GenerateJointLimitVisualizationData( - const Physics::JointLimitConfiguration& configuration, - const AZ::Quaternion& parentRotation, - const AZ::Quaternion& childRotation, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - [[maybe_unused]] AZStd::vector& vertexBufferOut, - [[maybe_unused]] AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) - { - if (const auto d6JointConfiguration = azrtti_cast(&configuration)) - { - const AZ::u32 angularSubdivisionsClamped = AZ::GetClamp(angularSubdivisions, 4u, 32u); - const AZ::u32 radialSubdivisionsClamped = AZ::GetClamp(radialSubdivisions, 1u, 4u); - - const D6JointState jointState = CalculateD6JointState(parentRotation, d6JointConfiguration->m_parentLocalRotation, - childRotation, d6JointConfiguration->m_childLocalRotation); - const float swingAngleY = jointState.m_swingAngleY; - const float swingAngleZ = jointState.m_swingAngleZ; - const float twistAngle = jointState.m_twistAngle; - const float swingLimitY = AZ::DegToRad(d6JointConfiguration->m_swingLimitY); - const float swingLimitZ = AZ::DegToRad(d6JointConfiguration->m_swingLimitZ); - const float twistLimitLower = AZ::DegToRad(d6JointConfiguration->m_twistLimitLower); - const float twistLimitUpper = AZ::DegToRad(d6JointConfiguration->m_twistLimitUpper); - - AppendD6SwingConeToLineBuffer(d6JointConfiguration->m_parentLocalRotation, swingAngleY, swingAngleZ, swingLimitY, - swingLimitZ, scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); - AppendD6TwistArcToLineBuffer(d6JointConfiguration->m_parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, - scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); - AppendD6CurrentTwistToLineBuffer(d6JointConfiguration->m_parentLocalRotation, twistAngle, twistLimitLower, - twistLimitUpper, scale, lineBufferOut, lineValidityBufferOut); - } - } - - AZStd::unique_ptr JointUtils::ComputeInitialJointLimitConfiguration( - const AZ::TypeId& jointLimitTypeId, - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Vector3& axis, - const AZStd::vector& exampleLocalRotations) - { - AZ_UNUSED(exampleLocalRotations); - - if (jointLimitTypeId == D6JointLimitConfiguration::RTTI_Type()) - { - const AZ::Vector3& normalizedAxis = axis.IsZero() - ? AZ::Vector3::CreateAxisX() - : axis.GetNormalized(); - - D6JointLimitConfiguration d6JointLimitConfig; - const AZ::Quaternion childLocalRotation = AZ::Quaternion::CreateShortestArc(AZ::Vector3::CreateAxisX(), - childWorldRotation.GetConjugate().TransformVector(normalizedAxis)); - d6JointLimitConfig.m_childLocalRotation = childLocalRotation; - d6JointLimitConfig.m_parentLocalRotation = parentWorldRotation.GetConjugate() * childWorldRotation * childLocalRotation; - - return AZStd::make_unique(d6JointLimitConfig); - } - - AZ_Warning("PhysX Joint Utils", false, "Unsupported joint type in ComputeInitialJointLimitConfiguration"); - return nullptr; - } - - const char* D6JointLimitConfiguration::GetTypeName() - { - return "D6 Joint"; - } - - void D6JointLimitConfiguration::Reflect(AZ::ReflectContext* context) - { - if (auto serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(1) - ->Field("SwingLimitY", &D6JointLimitConfiguration::m_swingLimitY) - ->Field("SwingLimitZ", &D6JointLimitConfiguration::m_swingLimitZ) - ->Field("TwistLowerLimit", &D6JointLimitConfiguration::m_twistLimitLower) - ->Field("TwistUpperLimit", &D6JointLimitConfiguration::m_twistLimitUpper) - ; - - AZ::EditContext* editContext = serializeContext->GetEditContext(); - if (editContext) - { - editContext->Class( - "PhysX D6 Joint Configuration", "") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) - ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_swingLimitY, "Swing limit Y", - "Maximum angle from the Y axis of the joint frame") - ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") - ->Attribute(AZ::Edit::Attributes::Min, JointConstants::MinSwingLimitDegrees) - ->Attribute(AZ::Edit::Attributes::Max, 180.0f) - ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_swingLimitZ, "Swing limit Z", - "Maximum angle from the Z axis of the joint frame") - ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") - ->Attribute(AZ::Edit::Attributes::Min, JointConstants::MinSwingLimitDegrees) - ->Attribute(AZ::Edit::Attributes::Max, 180.0f) - ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_twistLimitLower, "Twist lower limit", - "Lower limit for rotation about the X axis of the joint frame") - ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") - ->Attribute(AZ::Edit::Attributes::Min, -180.0f) - ->Attribute(AZ::Edit::Attributes::Max, 180.0f) - ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_twistLimitUpper, "Twist upper limit", - "Upper limit for rotation about the X axis of the joint frame") - ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") - ->Attribute(AZ::Edit::Attributes::Min, -180.0f) - ->Attribute(AZ::Edit::Attributes::Max, 180.0f) - ; - } - } - } -} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint.h b/Gems/PhysX/Code/Source/Joint.h deleted file mode 100644 index 01214a0fec..0000000000 --- a/Gems/PhysX/Code/Source/Joint.h +++ /dev/null @@ -1,311 +0,0 @@ -/* -* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -* its licensors. -* -* For complete copyright and license terms please see the LICENSE at the root of this -* distribution (the "License"). All use of this software is governed by the License, -* or, if provided, by the license below or the license accompanying this file. Do not -* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* -*/ - -#pragma once - -#include - -namespace AzPhysics -{ - struct SimulatedBody; -} - -namespace PhysX -{ - class D6JointLimitConfiguration - : public Physics::JointLimitConfiguration - { - public: - AZ_CLASS_ALLOCATOR(D6JointLimitConfiguration, AZ::SystemAllocator, 0); - AZ_RTTI(D6JointLimitConfiguration, "{90C5C23D-16C0-4F23-AD50-A190E402388E}", Physics::JointLimitConfiguration); - static void Reflect(AZ::ReflectContext* context); - - const char* GetTypeName() override; - - float m_swingLimitY = 45.0f; ///< Maximum angle in degrees from the Y axis of the joint frame. - float m_swingLimitZ = 45.0f; ///< Maximum angle in degrees from the Z axis of the joint frame. - float m_twistLimitLower = -45.0f; ///< Lower limit in degrees for rotation about the X axis of the joint frame. - float m_twistLimitUpper = 45.0f; ///< Upper limit in degrees for rotation about the X axis of the joint frame. - }; - - class Joint - : public Physics::Joint - { - public: - AZ_CLASS_ALLOCATOR(Joint, AZ::SystemAllocator, 0); - AZ_RTTI(Joint, "{3C739E22-8EF0-419F-966B-C575A1F5A08B}", Physics::Joint); - - Joint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody); - - virtual ~Joint() = default; - - AzPhysics::SimulatedBody* GetParentBody() const override; - AzPhysics::SimulatedBody* GetChildBody() const override; - void SetParentBody(AzPhysics::SimulatedBody* parentBody) override; - void SetChildBody(AzPhysics::SimulatedBody* childBody) override; - const AZStd::string& GetName() const override; - void SetName(const AZStd::string& name) override; - void* GetNativePointer() override; - - protected: - bool SetPxActors(); - - using PxJointUniquePtr = AZStd::unique_ptr>; - PxJointUniquePtr m_pxJoint; - AzPhysics::SimulatedBody* m_parentBody; - AzPhysics::SimulatedBody* m_childBody; - AZStd::string m_name; - }; - - class D6Joint - : public Joint - { - public: - AZ_CLASS_ALLOCATOR(D6Joint, AZ::SystemAllocator, 0); - AZ_RTTI(D6Joint, "{962C4044-2BD2-4E4C-913C-FB8E85A2A12A}", Joint); - - D6Joint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody) - : Joint(pxJoint, parentBody, childBody) - { - } - virtual ~D6Joint() = default; - - const AZ::Crc32 GetNativeType() const override; - void GenerateJointLimitVisualizationData( - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) override; - }; - - struct D6JointState - { - float m_swingAngleY; - float m_swingAngleZ; - float m_twistAngle; - }; - - /// A fixed joint locks 2 bodies relative to one another on all axes of freedom. - class FixedJoint : public Joint - { - public: - AZ_CLASS_ALLOCATOR(FixedJoint, AZ::SystemAllocator, 0); - AZ_TYPE_INFO(FixedJoint, "{203FB99C-7DC5-478A-A52C-A1F2AAF61FB8}"); - - FixedJoint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody) - : Joint(pxJoint, parentBody, childBody) - { - } - - const AZ::Crc32 GetNativeType() const override; - void GenerateJointLimitVisualizationData( - float /*scale*/, - AZ::u32 /*angularSubdivisions*/, - AZ::u32 /*radialSubdivisions*/, - AZStd::vector& /*vertexBufferOut*/, - AZStd::vector& /*indexBufferOut*/, - AZStd::vector& /*lineBufferOut*/, - AZStd::vector& /*lineValidityBufferOut*/) override {} - }; - - /// A hinge joint locks 2 bodies relative to one another except about the x-axis of the joint between them. - class HingeJoint : public Joint - { - public: - AZ_CLASS_ALLOCATOR(HingeJoint, AZ::SystemAllocator, 0); - AZ_TYPE_INFO(HingeJoint, "{8EFF1002-B08C-47CE-883C-82F0CF3736E0}"); - - HingeJoint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody) - : Joint(pxJoint, parentBody, childBody) - { - } - - const AZ::Crc32 GetNativeType() const override; - void GenerateJointLimitVisualizationData( - float /*scale*/, - AZ::u32 /*angularSubdivisions*/, - AZ::u32 /*radialSubdivisions*/, - AZStd::vector& /*vertexBufferOut*/, - AZStd::vector& /*indexBufferOut*/, - AZStd::vector& /*lineBufferOut*/, - AZStd::vector& /*lineValidityBufferOut*/) override {} - }; - - /// A ball joint locks 2 bodies relative to one another except about the y and z axes of the joint between them. - class BallJoint : public Joint - { - public: - AZ_CLASS_ALLOCATOR(BallJoint, AZ::SystemAllocator, 0); - AZ_TYPE_INFO(BallJoint, "{9FADA1C2-0E2F-4E1B-9E83-6292A1606372}"); - - BallJoint(physx::PxJoint* pxJoint, AzPhysics::SimulatedBody* parentBody, - AzPhysics::SimulatedBody* childBody) - : Joint(pxJoint, parentBody, childBody) - { - } - - const AZ::Crc32 GetNativeType() const override; - void GenerateJointLimitVisualizationData( - float /*scale*/, - AZ::u32 /*angularSubdivisions*/, - AZ::u32 /*radialSubdivisions*/, - AZStd::vector& /*vertexBufferOut*/, - AZStd::vector& /*indexBufferOut*/, - AZStd::vector& /*lineBufferOut*/, - AZStd::vector& /*lineValidityBufferOut*/) override {} - }; - - /// Common parameters for all physics joint types. - class GenericJointConfiguration - { - public: - enum class GenericJointFlag : AZ::u16 - { - None = 0, - Breakable = 1, - SelfCollide = 1 << 1 - }; - - AZ_CLASS_ALLOCATOR(GenericJointConfiguration, AZ::SystemAllocator, 0); - AZ_TYPE_INFO(GenericJointConfiguration, "{AB2E2F92-0248-48A8-9DDD-21284AF0C1DF}"); - static void Reflect(AZ::ReflectContext* context); - static bool VersionConverter( - AZ::SerializeContext& context, - AZ::SerializeContext::DataElementNode& classElement); - - GenericJointConfiguration() = default; - GenericJointConfiguration(float forceMax, - float torqueMax, - AZ::Transform localTransformFromFollower, - AZ::EntityId leadEntity, - AZ::EntityId followerEntity, - GenericJointFlag flags); - - bool GetFlag(GenericJointFlag flag); ///< Returns if a particular flag is set as a bool. - - GenericJointFlag m_flags = GenericJointFlag::None; ///< Flags that indicates if joint is breakable, self-colliding, etc.. Converting joint between breakable/non-breakable at game time is allowed. - float m_forceMax = 1.0f; ///< Max force joint can tolerate before breaking. - float m_torqueMax = 1.0f; ///< Max torque joint can tolerate before breaking. - AZ::EntityId m_leadEntity; ///< EntityID for entity containing body that is lead to this joint constraint. - AZ::EntityId m_followerEntity; ///< EntityID for entity containing body that is follower to this joint constraint. - AZ::Transform m_localTransformFromFollower; ///< Joint's location and orientation in the frame (coordinate system) of the follower entity. - }; - AZ_DEFINE_ENUM_BITWISE_OPERATORS(PhysX::GenericJointConfiguration::GenericJointFlag) - - /// Generic pair of limit values for joint types, e.g. a pair of angular values. - /// This is different from JointLimitConfiguration used in non-generic joints for character/ragdoll/animation. - class GenericJointLimitsConfiguration - { - public: - AZ_CLASS_ALLOCATOR(GenericJointLimitsConfiguration, AZ::SystemAllocator, 0); - AZ_TYPE_INFO(GenericJointLimitsConfiguration, "{9D129B49-F4E6-4F2A-B94D-AC2D6AC6CE02}"); - static void Reflect(AZ::ReflectContext* context); - - GenericJointLimitsConfiguration() = default; - GenericJointLimitsConfiguration(float damping - , bool isLimited - , bool isSoftLimit - , float limitFirst - , float limitSecond - , float stiffness - , float tolerance); - - bool m_isLimited = true; ///< Specifies if limits are applied to the joint constraints. E.g. if the swing angles are limited. - bool m_isSoftLimit = false; ///< If limit is soft, spring and damping are used, otherwise tolerance is used. Converting between soft/hard limit at game time is allowed. - float m_damping = 20.0f; ///< The damping strength of the drive, the force proportional to the velocity error. Used if limit is soft. - float m_limitFirst = 45.0f; ///< Positive angle limit in the case of twist angle limits, Y-axis swing limit in the case of cone limits. - float m_limitSecond = 45.0f; ///< Negative angle limit in the case of twist angle limits, Z-axis swing limit in the case of cone limits. - float m_stiffness = 100.0f; ///< The spring strength of the drive, the force proportional to the position error. Used if limit is soft. - float m_tolerance = 0.1f; ///< Distance from the joint at which limits becomes enforced. Used if limit is hard. - }; - - class JointUtils - { - public: - static AZStd::vector GetSupportedJointTypes(); - - static AZStd::shared_ptr CreateJointLimitConfiguration(AZ::TypeId jointType); - - static AZStd::shared_ptr CreateJoint(const AZStd::shared_ptr& configuration, - AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody); - - static D6JointState CalculateD6JointState( - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& parentLocalRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Quaternion& childLocalRotation); - - static bool IsD6SwingValid( - float swingAngleY, - float swingAngleZ, - float swingLimitY, - float swingLimitZ); - - static void AppendD6SwingConeToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float swingAngleY, - float swingAngleZ, - float swingLimitY, - float swingLimitZ, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut); - - static void AppendD6TwistArcToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float twistAngle, - float twistLimitLower, - float twistLimitUpper, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut); - - static void AppendD6CurrentTwistToLineBuffer( - const AZ::Quaternion& parentLocalRotation, - float twistAngle, - float twistLimitLower, - float twistLimitUpper, - float scale, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut); - - static void GenerateJointLimitVisualizationData( - const Physics::JointLimitConfiguration& configuration, - const AZ::Quaternion& parentRotation, - const AZ::Quaternion& childRotation, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut); - - static AZStd::unique_ptr ComputeInitialJointLimitConfiguration( - const AZ::TypeId& jointLimitTypeId, - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Vector3& axis, - const AZStd::vector& exampleLocalRotations); - }; -} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint/Configuration/PhysXJointConfiguration.cpp b/Gems/PhysX/Code/Source/Joint/Configuration/PhysXJointConfiguration.cpp new file mode 100644 index 0000000000..0d1fe9c988 --- /dev/null +++ b/Gems/PhysX/Code/Source/Joint/Configuration/PhysXJointConfiguration.cpp @@ -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. +* +*/ + +#include +#include +#include +#include +#include + +namespace PhysX +{ + JointGenericProperties::JointGenericProperties(GenericJointFlag flags, float forceMax, float torqueMax) + : m_flags(flags) + , m_forceMax(forceMax) + , m_torqueMax(torqueMax) + { + + } + + JointLimitProperties::JointLimitProperties( + bool isLimited, bool isSoftLimit, + float damping, float limitFirst, float limitSecond, float stiffness, float tolerance) + : m_isLimited(isLimited) + , m_isSoftLimit(isSoftLimit) + , m_damping(damping) + , m_limitFirst(limitFirst) + , m_limitSecond(limitSecond) + , m_stiffness(stiffness) + , m_tolerance(tolerance) + { + + } + + bool JointGenericProperties::IsFlagSet(GenericJointFlag flag) const + { + return static_cast(m_flags & flag); + } + + void D6JointLimitConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("SwingLimitY", &D6JointLimitConfiguration::m_swingLimitY) + ->Field("SwingLimitZ", &D6JointLimitConfiguration::m_swingLimitZ) + ->Field("TwistLowerLimit", &D6JointLimitConfiguration::m_twistLimitLower) + ->Field("TwistUpperLimit", &D6JointLimitConfiguration::m_twistLimitUpper) + ; + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class( + "PhysX D6 Joint Configuration", "") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_swingLimitY, "Swing limit Y", + "Maximum angle from the Y axis of the joint frame") + ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") + ->Attribute(AZ::Edit::Attributes::Min, JointConstants::MinSwingLimitDegrees) + ->Attribute(AZ::Edit::Attributes::Max, 180.0f) + ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_swingLimitZ, "Swing limit Z", + "Maximum angle from the Z axis of the joint frame") + ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") + ->Attribute(AZ::Edit::Attributes::Min, JointConstants::MinSwingLimitDegrees) + ->Attribute(AZ::Edit::Attributes::Max, 180.0f) + ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_twistLimitLower, "Twist lower limit", + "Lower limit for rotation about the X axis of the joint frame") + ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") + ->Attribute(AZ::Edit::Attributes::Min, -180.0f) + ->Attribute(AZ::Edit::Attributes::Max, 180.0f) + ->DataElement(AZ::Edit::UIHandlers::Default, &D6JointLimitConfiguration::m_twistLimitUpper, "Twist upper limit", + "Upper limit for rotation about the X axis of the joint frame") + ->Attribute(AZ::Edit::Attributes::Suffix, " degrees") + ->Attribute(AZ::Edit::Attributes::Min, -180.0f) + ->Attribute(AZ::Edit::Attributes::Max, 180.0f) + ; + } + } + } + + void JointGenericProperties::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("Maximum Force", &JointGenericProperties::m_forceMax) + ->Field("Maximum Torque", &JointGenericProperties::m_torqueMax) + ->Field("Flags", &JointGenericProperties::m_flags) + ; + } + } + + void JointLimitProperties::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("First Limit", &JointLimitProperties::m_limitFirst) + ->Field("Second Limit", &JointLimitProperties::m_limitSecond) + ->Field("Tolerance", &JointLimitProperties::m_tolerance) + ->Field("Is Limited", &JointLimitProperties::m_isLimited) + ->Field("Is Soft Limit", &JointLimitProperties::m_isSoftLimit) + ->Field("Damping", &JointLimitProperties::m_damping) + ->Field("Spring", &JointLimitProperties::m_stiffness) + ; + } + } + + void FixedJointConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("Generic Properties", &FixedJointConfiguration::m_genericProperties) + ; + } + } + + void BallJointConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("Generic Properties", &BallJointConfiguration::m_genericProperties) + ->Field("Limit Properties", &BallJointConfiguration::m_limitProperties) + ; + } + } + + void HingeJointConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("Generic Properties", &HingeJointConfiguration::m_genericProperties) + ->Field("Limit Properties", &HingeJointConfiguration::m_limitProperties) + ; + } + } +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJoint.cpp b/Gems/PhysX/Code/Source/Joint/PhysXJoint.cpp new file mode 100644 index 0000000000..53f054c1df --- /dev/null +++ b/Gems/PhysX/Code/Source/Joint/PhysXJoint.cpp @@ -0,0 +1,200 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include +#include +#include +#include +#include +#include + +namespace PhysX +{ + AzPhysics::SimulatedBodyHandle PhysXJoint::GetParentBodyHandle() const + { + return m_parentBodyHandle; + } + + AzPhysics::SimulatedBodyHandle PhysXJoint::GetChildBodyHandle() const + { + return m_childBodyHandle; + } + + PhysXJoint::PhysXJoint( + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + : m_sceneHandle(sceneHandle) + , m_parentBodyHandle(parentBodyHandle) + , m_childBodyHandle(childBodyHandle) + { + + } + + bool PhysXJoint::SetPxActors() + { + physx::PxRigidActor* parentActor = Utils::GetPxRigidActor(m_sceneHandle, m_parentBodyHandle); + physx::PxRigidActor* childActor = Utils::GetPxRigidActor(m_sceneHandle, m_childBodyHandle); + if (!parentActor && !childActor) + { + AZ_Error("PhysX Joint", false, "Invalid PhysX actors in joint - at least one must be a PxRigidActor."); + return false; + } + + m_pxJoint->setActors(parentActor, childActor); + return true; + } + + void PhysXJoint::SetParentBody(AzPhysics::SimulatedBodyHandle parentBodyHandle) + { + auto* parentBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, parentBodyHandle); + auto* childBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, m_childBodyHandle); + + if (Utils::IsAtLeastOneDynamic(parentBody, childBody)) + { + m_parentBodyHandle = parentBodyHandle; + SetPxActors(); + } + else + { + AZ_Warning("PhysX Joint", false, "Call to SetParentBody would result in invalid joint - at least one " + "body in a joint must be dynamic."); + } + } + + void PhysXJoint::SetChildBody(AzPhysics::SimulatedBodyHandle childBodyHandle) + { + auto* parentBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, m_parentBodyHandle); + auto* childBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, childBodyHandle); + + if (Utils::IsAtLeastOneDynamic(parentBody, childBody)) + { + m_childBodyHandle = childBodyHandle; + SetPxActors(); + } + else + { + AZ_Warning("PhysX Joint", false, "Call to SetChildBody would result in invalid joint - at least one " + "body in a joint must be dynamic."); + } + } + + void* PhysXJoint::GetNativePointer() const + { + return m_pxJoint.get(); + } + + PhysXD6Joint::PhysXD6Joint(const D6JointLimitConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + : PhysXJoint(sceneHandle, parentBodyHandle, childBodyHandle) + { + m_pxJoint = Utils::PxJointFactories::CreatePxD6Joint(configuration, sceneHandle, parentBodyHandle, childBodyHandle); + } + + PhysXFixedJoint::PhysXFixedJoint(const FixedJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + : PhysXJoint(sceneHandle, parentBodyHandle, childBodyHandle) + { + m_pxJoint = Utils::PxJointFactories::CreatePxFixedJoint(configuration, sceneHandle, parentBodyHandle, childBodyHandle); + } + + PhysXBallJoint::PhysXBallJoint(const BallJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + : PhysXJoint(sceneHandle, parentBodyHandle, childBodyHandle) + { + m_pxJoint = Utils::PxJointFactories::CreatePxBallJoint(configuration, sceneHandle, parentBodyHandle, childBodyHandle); + } + + PhysXHingeJoint::PhysXHingeJoint(const HingeJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + : PhysXJoint(sceneHandle, parentBodyHandle, childBodyHandle) + { + m_pxJoint = Utils::PxJointFactories::CreatePxHingeJoint(configuration, sceneHandle, parentBodyHandle, childBodyHandle); + } + + AZ::Crc32 PhysXD6Joint::GetNativeType() const + { + return NativeTypeIdentifiers::D6Joint; + } + + AZ::Crc32 PhysXFixedJoint::GetNativeType() const + { + return NativeTypeIdentifiers::FixedJoint; + } + + AZ::Crc32 PhysXBallJoint::GetNativeType() const + { + return NativeTypeIdentifiers::BallJoint; + } + + AZ::Crc32 PhysXHingeJoint::GetNativeType() const + { + return NativeTypeIdentifiers::HingeJoint; + } + + void PhysXD6Joint::GenerateJointLimitVisualizationData( + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + [[maybe_unused]] AZStd::vector& vertexBufferOut, + [[maybe_unused]] AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + auto* parentBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, m_parentBodyHandle); + auto* childBody = Utils::GetSimulatedBodyFromHandle(m_sceneHandle, m_childBodyHandle); + + const AZ::u32 angularSubdivisionsClamped = AZ::GetClamp(angularSubdivisions, 4u, 32u); + const AZ::u32 radialSubdivisionsClamped = AZ::GetClamp(radialSubdivisions, 1u, 4u); + + const physx::PxD6Joint* joint = static_cast(m_pxJoint.get()); + const AZ::Quaternion parentLocalRotation = PxMathConvert(joint->getLocalPose(physx::PxJointActorIndex::eACTOR0).q); + const AZ::Quaternion parentWorldRotation = parentBody ? parentBody->GetOrientation() : AZ::Quaternion::CreateIdentity(); + const AZ::Quaternion childLocalRotation = PxMathConvert(joint->getLocalPose(physx::PxJointActorIndex::eACTOR1).q); + const AZ::Quaternion childWorldRotation = childBody ? childBody->GetOrientation() : AZ::Quaternion::CreateIdentity(); + + const float swingAngleY = joint->getSwingYAngle(); + const float swingAngleZ = joint->getSwingZAngle(); + const float swingLimitY = joint->getSwingLimit().yAngle; + const float swingLimitZ = joint->getSwingLimit().zAngle; + const float twistAngle = joint->getTwist(); + const float twistLimitLower = joint->getTwistLimit().lower; + const float twistLimitUpper = joint->getTwistLimit().upper; + + Utils::Joints::AppendD6SwingConeToLineBuffer( + parentLocalRotation, swingAngleY, swingAngleZ, swingLimitY, swingLimitZ, + scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); + Utils::Joints::AppendD6TwistArcToLineBuffer( + parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, + scale, angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); + Utils::Joints::AppendD6CurrentTwistToLineBuffer( + parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, + scale, lineBufferOut, lineValidityBufferOut); + + // draw the X-axis of the child joint frame + // make the axis slightly longer than the radius of the twist arc so that it is easy to see + float axisLength = 1.25f * scale; + AZ::Vector3 childAxis = (parentWorldRotation.GetConjugate() * childWorldRotation * childLocalRotation).TransformVector( + AZ::Vector3::CreateAxisX(axisLength)); + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(childAxis); + } +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJoint.h b/Gems/PhysX/Code/Source/Joint/PhysXJoint.h new file mode 100644 index 0000000000..549025d951 --- /dev/null +++ b/Gems/PhysX/Code/Source/Joint/PhysXJoint.h @@ -0,0 +1,126 @@ +/* +* 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 PhysX +{ + class PhysXJoint + : public AzPhysics::Joint + { + public: + AZ_CLASS_ALLOCATOR(PhysXJoint, AZ::SystemAllocator, 0); + AZ_RTTI(PhysXJoint, "{DBE1D185-E318-407D-A5A1-AC1DE7F4A62D}", AzPhysics::Joint); + + PhysXJoint( + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + virtual ~PhysXJoint() = default; + + AzPhysics::SimulatedBodyHandle GetParentBodyHandle() const override; + AzPhysics::SimulatedBodyHandle GetChildBodyHandle() const override; + void SetParentBody(AzPhysics::SimulatedBodyHandle parentBody) override; + void SetChildBody(AzPhysics::SimulatedBodyHandle childBody) override; + void* GetNativePointer() const override; + + protected: + bool SetPxActors(); + + Utils::PxJointUniquePtr m_pxJoint; + AzPhysics::SceneHandle m_sceneHandle; + AzPhysics::SimulatedBodyHandle m_parentBodyHandle; + AzPhysics::SimulatedBodyHandle m_childBodyHandle; + AZStd::string m_name; + }; + + class PhysXD6Joint + : public PhysXJoint + { + public: + AZ_CLASS_ALLOCATOR(PhysXD6Joint, AZ::SystemAllocator, 0); + AZ_RTTI(PhysXD6Joint, "{144B2FAF-A3EE-4FE1-9328-2C44FE1E3676}", PhysX::PhysXJoint); + + PhysXD6Joint(const D6JointLimitConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + virtual ~PhysXD6Joint() = default; + + AZ::Crc32 GetNativeType() const override; + void GenerateJointLimitVisualizationData( + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& vertexBufferOut, + AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) override; + }; + + //! A fixed joint locks 2 bodies relative to one another on all axes of freedom. + class PhysXFixedJoint : public PhysXJoint + { + public: + AZ_CLASS_ALLOCATOR(PhysXFixedJoint, AZ::SystemAllocator, 0); + AZ_RTTI(PhysXFixedJoint, "{B821D6D8-7B41-479D-9325-F9BC9754C5F8}", PhysX::PhysXJoint); + + PhysXFixedJoint(const FixedJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + virtual ~PhysXFixedJoint() = default; + + AZ::Crc32 GetNativeType() const override; + }; + + //! A ball joint locks 2 bodies relative to one another except about the y and z axes of the joint between them. + class PhysXBallJoint : public PhysXJoint + { + public: + AZ_CLASS_ALLOCATOR(PhysXBallJoint, AZ::SystemAllocator, 0); + AZ_RTTI(PhysXBallJoint, "{9494CE43-3AE2-40AB-ADF7-FDC5F8B0F15A}", PhysX::PhysXJoint); + + PhysXBallJoint(const BallJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + virtual ~PhysXBallJoint() = default; + + AZ::Crc32 GetNativeType() const override; + }; + + //! A hinge joint locks 2 bodies relative to one another except about the x-axis of the joint between them. + class PhysXHingeJoint : public PhysXJoint + { + public: + AZ_CLASS_ALLOCATOR(PhysXHingeJoint, AZ::SystemAllocator, 0); + AZ_RTTI(PhysXHingeJoint, "{9C5B955C-6C80-45FA-855D-DDA449C85313}", PhysX::PhysXJoint); + + PhysXHingeJoint(const HingeJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + virtual ~PhysXHingeJoint() = default; + + AZ::Crc32 GetNativeType() const override; + }; +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp new file mode 100644 index 0000000000..5ab4564e0e --- /dev/null +++ b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp @@ -0,0 +1,470 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace PhysX { + namespace Utils + { + struct PxJointActorData + { + static PxJointActorData InvalidPxJointActorData; + + physx::PxRigidActor* parentActor = nullptr; + physx::PxRigidActor* childActor = nullptr; + }; + PxJointActorData PxJointActorData::InvalidPxJointActorData; + + PxJointActorData GetJointPxActors( + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + { + auto* parentBody = GetSimulatedBodyFromHandle(sceneHandle, parentBodyHandle); + auto* childBody = GetSimulatedBodyFromHandle(sceneHandle, childBodyHandle); + + if (!IsAtLeastOneDynamic(parentBody, childBody)) + { + AZ_Warning("PhysX Joint", false, "CreateJoint failed - at least one body must be dynamic."); + return PxJointActorData::InvalidPxJointActorData; + } + + physx::PxRigidActor* parentActor = GetPxRigidActor(sceneHandle, parentBodyHandle); + physx::PxRigidActor* childActor = GetPxRigidActor(sceneHandle, childBodyHandle); + + if (!parentActor && !childActor) + { + AZ_Warning("PhysX Joint", false, "CreateJoint failed - at least one body must be a PxRigidActor."); + return PxJointActorData::InvalidPxJointActorData; + } + + return PxJointActorData{ + parentActor, + childActor + }; + } + + bool IsAtLeastOneDynamic(AzPhysics::SimulatedBody* body0, + AzPhysics::SimulatedBody* body1) + { + for (const AzPhysics::SimulatedBody* body : { body0, body1 }) + { + if (body) + { + if (body->GetNativeType() == NativeTypeIdentifiers::RigidBody || + body->GetNativeType() == NativeTypeIdentifiers::ArticulationLink) + { + return true; + } + } + } + return false; + } + + physx::PxRigidActor* GetPxRigidActor(AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle worldBodyHandle) + { + auto* worldBody = GetSimulatedBodyFromHandle(sceneHandle, worldBodyHandle); + if (worldBody != nullptr + && static_cast(worldBody->GetNativePointer())->is()) + { + return static_cast(worldBody->GetNativePointer()); + } + + return nullptr; + } + + void ReleasePxJoint(physx::PxJoint* joint) + { + PHYSX_SCENE_WRITE_LOCK(joint->getScene()); + joint->userData = nullptr; + joint->release(); + } + + AzPhysics::SimulatedBody* GetSimulatedBodyFromHandle(AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle bodyHandle) + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + return sceneInterface->GetSimulatedBodyFromHandle(sceneHandle, bodyHandle); + } + return nullptr; + } + + void InitializeGenericProperties(const JointGenericProperties& properties, physx::PxJoint* nativeJoint) + { + if (!nativeJoint) + { + return; + } + PHYSX_SCENE_WRITE_LOCK(nativeJoint->getScene()); + nativeJoint->setConstraintFlag( + physx::PxConstraintFlag::eCOLLISION_ENABLED, + properties.IsFlagSet(JointGenericProperties::GenericJointFlag::SelfCollide)); + + if (properties.IsFlagSet(JointGenericProperties::GenericJointFlag::Breakable)) + { + nativeJoint->setBreakForce(properties.m_forceMax, properties.m_torqueMax); + } + } + + void InitializeSphericalLimitProperties(const JointLimitProperties& properties, physx::PxSphericalJoint* nativeJoint) + { + if (!nativeJoint) + { + return; + } + + if (!properties.m_isLimited) + { + nativeJoint->setSphericalJointFlag(physx::PxSphericalJointFlag::eLIMIT_ENABLED, false); + return; + } + + // Hard limit uses a tolerance value (distance to limit at which limit becomes active). + // Soft limit allows angle to exceed limit but springs back with configurable spring stiffness and damping. + physx::PxJointLimitCone swingLimit( + AZ::DegToRad(properties.m_limitFirst), + AZ::DegToRad(properties.m_limitSecond), + properties.m_tolerance); + + if (properties.m_isSoftLimit) + { + swingLimit.stiffness = properties.m_stiffness; + swingLimit.damping = properties.m_damping; + } + + nativeJoint->setLimitCone(swingLimit); + nativeJoint->setSphericalJointFlag(physx::PxSphericalJointFlag::eLIMIT_ENABLED, true); + } + + void InitializeRevoluteLimitProperties(const JointLimitProperties& properties, physx::PxRevoluteJoint* nativeJoint) + { + if (!nativeJoint) + { + return; + } + + if (!properties.m_isLimited) + { + nativeJoint->setRevoluteJointFlag(physx::PxRevoluteJointFlag::eLIMIT_ENABLED, false); + return; + } + + physx::PxJointAngularLimitPair limitPair( + AZ::DegToRad(properties.m_limitSecond), + AZ::DegToRad(properties.m_limitFirst), + properties.m_tolerance); + + if (properties.m_isSoftLimit) + { + limitPair.stiffness = properties.m_stiffness; + limitPair.damping = properties.m_damping; + } + + nativeJoint->setLimit(limitPair); + nativeJoint->setRevoluteJointFlag(physx::PxRevoluteJointFlag::eLIMIT_ENABLED, true); + } + + namespace PxJointFactories + { + PxJointUniquePtr CreatePxD6Joint( + const PhysX::D6JointLimitConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + { + PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); + + if (!actorData.parentActor || !actorData.childActor) + { + return nullptr; + } + + const physx::PxTransform parentWorldTransform = + actorData.parentActor ? actorData.parentActor->getGlobalPose() : physx::PxTransform(physx::PxIdentity); + const physx::PxTransform childWorldTransform = + actorData.childActor ? actorData.childActor->getGlobalPose() : physx::PxTransform(physx::PxIdentity); + const physx::PxVec3 childOffset = childWorldTransform.p - parentWorldTransform.p; + physx::PxTransform parentLocalTransform(PxMathConvert(configuration.m_parentLocalRotation).getNormalized()); + const physx::PxTransform childLocalTransform(PxMathConvert(configuration.m_childLocalRotation).getNormalized()); + parentLocalTransform.p = parentWorldTransform.q.rotateInv(childOffset); + + physx::PxD6Joint* joint = PxD6JointCreate(PxGetPhysics(), + actorData.parentActor, parentLocalTransform, actorData.childActor, childLocalTransform); + + joint->setMotion(physx::PxD6Axis::eTWIST, physx::PxD6Motion::eLIMITED); + joint->setMotion(physx::PxD6Axis::eSWING1, physx::PxD6Motion::eLIMITED); + joint->setMotion(physx::PxD6Axis::eSWING2, physx::PxD6Motion::eLIMITED); + + AZ_Warning("PhysX Joint", + configuration.m_swingLimitY >= JointConstants::MinSwingLimitDegrees && configuration.m_swingLimitZ >= JointConstants::MinSwingLimitDegrees, + "Very small swing limit requested for joint between \"%s\" and \"%s\", increasing to %f degrees to improve stability", + actorData.parentActor ? actorData.parentActor->getName() : "world", + actorData.childActor ? actorData.childActor->getName() : "world", + JointConstants::MinSwingLimitDegrees); + + const float swingLimitY = AZ::DegToRad(AZ::GetMax(JointConstants::MinSwingLimitDegrees, configuration.m_swingLimitY)); + const float swingLimitZ = AZ::DegToRad(AZ::GetMax(JointConstants::MinSwingLimitDegrees, configuration.m_swingLimitZ)); + physx::PxJointLimitCone limitCone(swingLimitY, swingLimitZ); + joint->setSwingLimit(limitCone); + + const float twistLower = AZ::DegToRad(AZStd::GetMin(configuration.m_twistLimitLower, configuration.m_twistLimitUpper)); + const float twistUpper = AZ::DegToRad(AZStd::GetMax(configuration.m_twistLimitLower, configuration.m_twistLimitUpper)); + physx::PxJointAngularLimitPair twistLimitPair(twistLower, twistUpper); + joint->setTwistLimit(twistLimitPair); + + return Utils::PxJointUniquePtr(joint, ReleasePxJoint); + } + + PxJointUniquePtr CreatePxFixedJoint( + const PhysX::FixedJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + { + PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); + + if (!actorData.parentActor || !actorData.childActor) + { + return nullptr; + } + + physx::PxFixedJoint* joint; + const AZ::Transform parentLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_parentLocalRotation, configuration.m_parentLocalPosition); + const AZ::Transform childLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_childLocalRotation, configuration.m_childLocalPosition); + + { + PHYSX_SCENE_READ_LOCK(actorData.childActor->getScene()); + joint = physx::PxFixedJointCreate(PxGetPhysics(), + actorData.parentActor, PxMathConvert(parentLocalTM), + actorData.childActor, PxMathConvert(childLocalTM)); + } + + InitializeGenericProperties( + configuration.m_genericProperties, + static_cast(joint)); + + return Utils::PxJointUniquePtr(joint, ReleasePxJoint); + } + + PxJointUniquePtr CreatePxBallJoint( + const PhysX::BallJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + { + PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); + + if (!actorData.parentActor || !actorData.childActor) + { + return nullptr; + } + + physx::PxSphericalJoint* joint; + const AZ::Transform parentLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_parentLocalRotation, configuration.m_parentLocalPosition); + const AZ::Transform childLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_childLocalRotation, configuration.m_childLocalPosition); + + { + PHYSX_SCENE_READ_LOCK(actorData.childActor->getScene()); + joint = physx::PxSphericalJointCreate(PxGetPhysics(), + actorData.parentActor, PxMathConvert(parentLocalTM), + actorData.childActor, PxMathConvert(childLocalTM)); + } + + InitializeSphericalLimitProperties(configuration.m_limitProperties, joint); + InitializeGenericProperties( + configuration.m_genericProperties, + static_cast(joint)); + + return Utils::PxJointUniquePtr(joint, ReleasePxJoint); + } + + PxJointUniquePtr CreatePxHingeJoint( + const PhysX::HingeJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle) + { + PxJointActorData actorData = GetJointPxActors(sceneHandle, parentBodyHandle, childBodyHandle); + + if (!actorData.parentActor || !actorData.childActor) + { + return nullptr; + } + + physx::PxRevoluteJoint* joint; + const AZ::Transform parentLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_parentLocalRotation, configuration.m_parentLocalPosition); + const AZ::Transform childLocalTM = AZ::Transform::CreateFromQuaternionAndTranslation( + configuration.m_childLocalRotation, configuration.m_childLocalPosition); + + { + PHYSX_SCENE_READ_LOCK(actorData.childActor->getScene()); + joint = physx::PxRevoluteJointCreate(PxGetPhysics(), + actorData.parentActor, PxMathConvert(parentLocalTM), + actorData.childActor, PxMathConvert(childLocalTM)); + } + + InitializeRevoluteLimitProperties(configuration.m_limitProperties, joint); + InitializeGenericProperties( + configuration.m_genericProperties, + static_cast(joint)); + + return Utils::PxJointUniquePtr(joint, ReleasePxJoint); + } + } // namespace PxJointFactories + + namespace Joints + { + bool IsD6SwingValid(float swingAngleY, float swingAngleZ, float swingLimitY, float swingLimitZ) + { + const float epsilon = AZ::Constants::FloatEpsilon; + const float yFactor = AZStd::tan(0.25f * swingAngleY) / AZStd::GetMax(epsilon, AZStd::tan(0.25f * swingLimitY)); + const float zFactor = AZStd::tan(0.25f * swingAngleZ) / AZStd::GetMax(epsilon, AZStd::tan(0.25f * swingLimitZ)); + + return (yFactor * yFactor + zFactor * zFactor <= 1.0f + epsilon); + } + + void AppendD6SwingConeToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float swingAngleY, + float swingAngleZ, + float swingLimitY, + float swingLimitZ, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::u32 numLinesSwingCone = angularSubdivisions * (1u + radialSubdivisions); + lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesSwingCone); + lineValidityBufferOut.reserve(lineValidityBufferOut.size() + numLinesSwingCone); + + // the orientation quat for a radial line in the cone can be represented in terms of sin and cos half angles + // these expressions can be efficiently calculated using tan quarter angles as follows: + // writing t = tan(x / 4) + // sin(x / 2) = 2 * t / (1 + t * t) + // cos(x / 2) = (1 - t * t) / (1 + t * t) + const float tanQuarterSwingZ = AZStd::tan(0.25f * swingLimitZ); + const float tanQuarterSwingY = AZStd::tan(0.25f * swingLimitY); + + AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); + for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) + { + const float angle = AZ::Constants::TwoPi / angularSubdivisions * angularIndex; + // the axis about which to rotate the x-axis to get the radial vector for this segment of the cone + const AZ::Vector3 rotationAxis(0, -tanQuarterSwingY * sinf(angle), tanQuarterSwingZ * cosf(angle)); + const float normalizationFactor = rotationAxis.GetLengthSq(); + const AZ::Quaternion radialVectorRotation = 1.0f / (1.0f + normalizationFactor) * + AZ::Quaternion::CreateFromVector3AndValue(2.0f * rotationAxis, 1.0f - normalizationFactor); + const AZ::Vector3 radialVector = + (parentLocalRotation * radialVectorRotation).TransformVector(AZ::Vector3::CreateAxisX(scale)); + + if (angularIndex > 0) + { + for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) + { + float radiusFraction = 1.0f / radialSubdivisions * radialIndex; + lineBufferOut.push_back(radiusFraction * radialVector); + lineBufferOut.push_back(radiusFraction * previousRadialVector); + } + } + + if (angularIndex < angularSubdivisions) + { + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(radialVector); + } + + previousRadialVector = radialVector; + } + + const bool swingValid = IsD6SwingValid(swingAngleY, swingAngleZ, swingLimitY, swingLimitZ); + lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesSwingCone, swingValid); + } + + void AppendD6TwistArcToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + float twistLimitLower, + float twistLimitUpper, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::u32 numLinesTwistArc = angularSubdivisions * (1u + radialSubdivisions) + 1u; + lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesTwistArc); + + AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); + const float twistRange = twistLimitUpper - twistLimitLower; + + for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) + { + const float angle = twistLimitLower + twistRange / angularSubdivisions * angularIndex; + const AZ::Vector3 radialVector = + parentLocalRotation.TransformVector(scale * AZ::Vector3(0.0f, cosf(angle), sinf(angle))); + + if (angularIndex > 0) + { + for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) + { + const float radiusFraction = 1.0f / radialSubdivisions * radialIndex; + lineBufferOut.push_back(radiusFraction * radialVector); + lineBufferOut.push_back(radiusFraction * previousRadialVector); + } + } + + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(radialVector); + + previousRadialVector = radialVector; + } + + const bool twistValid = (twistAngle >= twistLimitLower && twistAngle <= twistLimitUpper); + lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesTwistArc, twistValid); + } + + void AppendD6CurrentTwistToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + [[maybe_unused]] float twistLimitLower, + [[maybe_unused]] float twistLimitUpper, + float scale, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::Vector3 twistVector = + parentLocalRotation.TransformVector(1.25f * scale * AZ::Vector3(0.0f, cosf(twistAngle), sinf(twistAngle))); + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(twistVector); + lineValidityBufferOut.push_back(true); + } + } // namespace Joints + } // namespace Utils +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h new file mode 100644 index 0000000000..a129c6862a --- /dev/null +++ b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h @@ -0,0 +1,101 @@ +/* +* 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 + +namespace PhysX +{ + namespace JointConstants + { + // Setting swing limits to very small values can cause extreme stability problems, so clamp above a small + // threshold. + static const float MinSwingLimitDegrees = 1.0f; + } // namespace JointConstants + + namespace Utils + { + using PxJointUniquePtr = AZStd::unique_ptr>; + + bool IsAtLeastOneDynamic(AzPhysics::SimulatedBody* body0, AzPhysics::SimulatedBody* body1); + + physx::PxRigidActor* GetPxRigidActor(AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle worldBodyHandle); + AzPhysics::SimulatedBody* GetSimulatedBodyFromHandle( + AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle); + + namespace PxJointFactories + { + PxJointUniquePtr CreatePxD6Joint(const PhysX::D6JointLimitConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + PxJointUniquePtr CreatePxFixedJoint(const PhysX::FixedJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + PxJointUniquePtr CreatePxBallJoint(const PhysX::BallJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + + PxJointUniquePtr CreatePxHingeJoint(const PhysX::HingeJointConfiguration& configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle); + } // namespace PxActorFactories + + namespace Joints + { + bool IsD6SwingValid(float swingAngleY, float swingAngleZ, float swingLimitY, float swingLimitZ); + + void AppendD6SwingConeToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float swingAngleY, + float swingAngleZ, + float swingLimitY, + float swingLimitZ, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut); + + void AppendD6TwistArcToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + float twistLimitLower, + float twistLimitUpper, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut); + + void AppendD6CurrentTwistToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + float twistLimitLower, + float twistLimitUpper, + float scale, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut); + } // namespace Joints + } // namespace Utils +} // namespace PhysX + diff --git a/Gems/PhysX/Code/Source/JointComponent.cpp b/Gems/PhysX/Code/Source/JointComponent.cpp index 39fbe4a3fa..0d62cafa62 100644 --- a/Gems/PhysX/Code/Source/JointComponent.cpp +++ b/Gems/PhysX/Code/Source/JointComponent.cpp @@ -18,33 +18,65 @@ #include #include #include -#include #include #include +#include namespace PhysX { + JointComponentConfiguration::JointComponentConfiguration( + AZ::Transform localTransformFromFollower, + AZ::EntityId leadEntity, + AZ::EntityId followerEntity) + : m_localTransformFromFollower(localTransformFromFollower) + , m_leadEntity(leadEntity) + , m_followerEntity(followerEntity) + { + } + + void JointComponentConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(2) + ->Field("Follower Local Transform", &JointComponentConfiguration::m_localTransformFromFollower) + ->Field("Lead Entity", &JointComponentConfiguration::m_leadEntity) + ->Field("Follower Entity", &JointComponentConfiguration::m_followerEntity) + ; + } + } + void JointComponent::Reflect(AZ::ReflectContext* context) { + JointComponentConfiguration::Reflect(context); + if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(1) + ->Version(2) ->Field("Joint Configuration", &JointComponent::m_configuration) + ->Field("Joint Generic Properties", &JointComponent::m_genericProperties) ->Field("Joint Limits", &JointComponent::m_limits) ; } } - JointComponent::JointComponent(const GenericJointConfiguration& config) - : m_configuration(config) + JointComponent::JointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties) + : m_configuration(configuration) + , m_genericProperties(genericProperties) { } - JointComponent::JointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& limits) - : m_configuration(AZStd::move(config)) - , m_limits(limits) + JointComponent::JointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties) + : m_configuration(configuration) + , m_genericProperties(genericProperties) + , m_limits(limitProperties) { } @@ -67,16 +99,22 @@ namespace PhysX void JointComponent::Deactivate() { AZ::EntityBus::Handler::BusDisconnect(); - m_joint.reset(); + if (auto* physicsSystem = AZ::Interface::Get()) + { + if (auto* scene = physicsSystem->GetScene(m_jointSceneOwner)) + { + scene->RemoveJoint(m_jointHandle); + m_jointSceneOwner = AzPhysics::InvalidSceneHandle; + } + } } - physx::PxTransform JointComponent::GetJointLocalPose(const physx::PxRigidActor* actor - , const physx::PxTransform& jointPose) + AZ::Transform JointComponent::GetJointLocalPose(const physx::PxRigidActor* actor, const AZ::Transform& jointPose) { if (!actor) { AZ_Error("JointComponent::GetJointLocalPose", false, "Can't get pose for invalid actor pointer."); - return physx::PxTransform(); + return AZ::Transform::CreateIdentity(); } PHYSX_SCENE_READ_LOCK(actor->getScene()); @@ -84,41 +122,17 @@ namespace PhysX physx::PxTransform actorTranslateInv(-actorPose.p); physx::PxTransform actorRotateInv(actorPose.q); actorRotateInv = actorRotateInv.getInverse(); - return actorRotateInv * actorTranslateInv * jointPose; + return PxMathConvert(actorRotateInv * actorTranslateInv) * jointPose; } AZ::Transform JointComponent::GetJointTransform(AZ::EntityId entityId - , const GenericJointConfiguration& jointConfig) + , const JointComponentConfiguration& jointConfig) { AZ::Transform jointTransform = PhysX::Utils::GetEntityWorldTransformWithoutScale(entityId); jointTransform = jointTransform * jointConfig.m_localTransformFromFollower; return jointTransform; } - void JointComponent::InitGenericProperties() - { - if (!m_joint) - { - return; - } - - physx::PxJoint* jointNative = static_cast(m_joint->GetNativePointer()); - if (!jointNative) - { - return; - } - PHYSX_SCENE_WRITE_LOCK(jointNative->getScene()); - jointNative->setConstraintFlag( - physx::PxConstraintFlag::eCOLLISION_ENABLED, - m_configuration.GetFlag(GenericJointConfiguration::GenericJointFlag::SelfCollide)); - - if (m_configuration.GetFlag(GenericJointConfiguration::GenericJointFlag::Breakable)) - { - jointNative->setBreakForce(m_configuration.m_forceMax - , m_configuration.m_torqueMax); - } - } - void JointComponent::ObtainLeadFollowerInfo(JointComponent::LeadFollowerInfo& info) { info = LeadFollowerInfo(); @@ -160,16 +174,15 @@ namespace PhysX const AZ::Transform jointTransform = GetJointTransform(GetEntityId(), m_configuration); - physx::PxTransform jointPose = PxMathConvert(jointTransform); if (info.m_leadActor) { - info.m_leadLocal = GetJointLocalPose(info.m_leadActor, jointPose); // joint position & orientation in lead actor's frame. + info.m_leadLocal = GetJointLocalPose(info.m_leadActor, jointTransform); // joint position & orientation in lead actor's frame. } else { - info.m_leadLocal = jointPose; // lead is null, attaching follower to global position of joint. + info.m_leadLocal = jointTransform; // lead is null, attaching follower to global position of joint. } - info.m_followerLocal = PxMathConvert(m_configuration.m_localTransformFromFollower);// joint position & orientation in follower actor's frame. + info.m_followerLocal = m_configuration.m_localTransformFromFollower;// joint position & orientation in follower actor's frame. } void JointComponent::WarnInvalidJointSetup(AZ::EntityId entityId, const AZStd::string& message) @@ -189,7 +202,6 @@ namespace PhysX if (!m_configuration.m_leadEntity.IsValid() || entityId == m_configuration.m_leadEntity) { InitNativeJoint(); // Invoke overriden specific joint type instantiation - InitGenericProperties(); } // Else, follower entity is activated, subscribe to be notified that lead entity is activated. else diff --git a/Gems/PhysX/Code/Source/JointComponent.h b/Gems/PhysX/Code/Source/JointComponent.h index 468470dd37..6c63cc8ac3 100644 --- a/Gems/PhysX/Code/Source/JointComponent.h +++ b/Gems/PhysX/Code/Source/JointComponent.h @@ -18,7 +18,7 @@ #include #include -#include +#include namespace AzPhysics { @@ -27,6 +27,24 @@ namespace AzPhysics namespace PhysX { + class JointComponentConfiguration + { + public: + AZ_CLASS_ALLOCATOR(JointComponentConfiguration, AZ::SystemAllocator, 0); + AZ_TYPE_INFO(JointComponentConfiguration, "{1454F33F-AA6E-424B-A70C-9E463FBDEA19}"); + static void Reflect(AZ::ReflectContext* context); + + JointComponentConfiguration() = default; + JointComponentConfiguration( + AZ::Transform localTransformFromFollower, + AZ::EntityId leadEntity, + AZ::EntityId followerEntity); + + AZ::EntityId m_leadEntity; ///< EntityID for entity containing body that is lead to this joint constraint. + AZ::EntityId m_followerEntity; ///< EntityID for entity containing body that is follower to this joint constraint. + AZ::Transform m_localTransformFromFollower; ///< Joint's location and orientation in the frame (coordinate system) of the follower entity. + }; + /// Base class for game-time generic joint components. class JointComponent: public AZ::Component , protected AZ::EntityBus::Handler @@ -36,9 +54,13 @@ namespace PhysX static void Reflect(AZ::ReflectContext* context); JointComponent() = default; - explicit JointComponent(const GenericJointConfiguration& config); - JointComponent(const GenericJointConfiguration& config - , const GenericJointLimitsConfiguration& limits); + JointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties); + JointComponent( + const JointComponentConfiguration& configuration, + const JointGenericProperties& genericProperties, + const JointLimitProperties& limitProperties); protected: /// Struct to provide subclasses with native pointers during joint initialization. @@ -47,8 +69,8 @@ namespace PhysX { physx::PxRigidActor* m_leadActor = nullptr; physx::PxRigidActor* m_followerActor = nullptr; - physx::PxTransform m_leadLocal = physx::PxTransform(physx::PxIdentity); - physx::PxTransform m_followerLocal = physx::PxTransform(physx::PxIdentity); + AZ::Transform m_leadLocal = AZ::Transform::CreateIdentity(); + AZ::Transform m_followerLocal = AZ::Transform::CreateIdentity(); AzPhysics::SimulatedBody* m_leadBody = nullptr; AzPhysics::SimulatedBody* m_followerBody = nullptr; }; @@ -63,14 +85,10 @@ namespace PhysX /// Invoked in JointComponent::OnEntityActivated for specific joint types to instantiate native joint pointer. virtual void InitNativeJoint() {}; - physx::PxTransform GetJointLocalPose(const physx::PxRigidActor* actor, - const physx::PxTransform& jointPose); + AZ::Transform GetJointLocalPose(const physx::PxRigidActor* actor, const AZ::Transform& jointPose); AZ::Transform GetJointTransform(AZ::EntityId entityId, - const GenericJointConfiguration& jointConfig); - - /// Initializes joint properties common to all native joint types after native joint creation. - void InitGenericProperties(); + const JointComponentConfiguration& jointConfig); /// Used on initialization by sub-classes to get native pointers from entity IDs. /// This allows sub-classes to instantiate specific native types. This base class does not need knowledge of any specific joint type. @@ -79,8 +97,11 @@ namespace PhysX /// Issues warnings for invalid scenarios when initializing a joint from entity IDs. void WarnInvalidJointSetup(AZ::EntityId entityId, const AZStd::string& message); - GenericJointConfiguration m_configuration; - GenericJointLimitsConfiguration m_limits; - AZStd::shared_ptr m_joint = nullptr; + + JointComponentConfiguration m_configuration; + JointGenericProperties m_genericProperties; + JointLimitProperties m_limits; + AzPhysics::JointHandle m_jointHandle = AzPhysics::InvalidJointHandle; + AzPhysics::SceneHandle m_jointSceneOwner = AzPhysics::InvalidSceneHandle; }; } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp index 87e3304a90..90c8d1ad77 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp @@ -18,10 +18,10 @@ #include #include #include +#include #include #include #include -#include namespace PhysX { @@ -262,21 +262,24 @@ namespace PhysX physx::PxTransform parentTM(parentOffset); physx::PxTransform childTM(physx::PxIdentity); - AZStd::shared_ptr jointLimitConfig = configuration.m_nodes[nodeIndex].m_jointLimit; - if (!jointLimitConfig) + AZStd::shared_ptr jointConfig = configuration.m_nodes[nodeIndex].m_jointConfig; + if (!jointConfig) { - AZStd::vector supportedJointLimitTypes = JointUtils::GetSupportedJointTypes(); - - if (!supportedJointLimitTypes.empty()) - { - jointLimitConfig = JointUtils::CreateJointLimitConfiguration(supportedJointLimitTypes[0]); - } + jointConfig = AZStd::make_shared(); } + + AzPhysics::JointHandle jointHandle = sceneInterface->AddJoint( + sceneHandle, jointConfig.get(), + ragdoll->GetNode(parentIndex)->GetRigidBody().m_bodyHandle, + ragdoll->GetNode(nodeIndex)->GetRigidBody().m_bodyHandle); + + AzPhysics::Joint* joint = sceneInterface->GetJointFromHandle(sceneHandle, jointHandle); - AZStd::shared_ptr joint = JointUtils::CreateJoint( - jointLimitConfig, - &ragdoll->GetNode(parentIndex)->GetRigidBody(), - &ragdoll->GetNode(nodeIndex)->GetRigidBody()); + if (!joint) + { + AZ_Error("PhysX Ragdoll", false, "Failed to create joint for node index %i.", nodeIndex); + return nullptr; + } // Moving from PhysX 3.4 to 4.1, the allowed range of the twist angle was expanded from -pi..pi // to -2*pi..2*pi. diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/Ragdoll.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/API/Ragdoll.cpp index 4725212a9d..a489135d34 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/Ragdoll.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/Ragdoll.cpp @@ -378,8 +378,8 @@ namespace PhysX else { actor->setRigidBodyFlag(physx::PxRigidBodyFlag::eKINEMATIC, false); - const AZStd::shared_ptr& joint = m_nodes[nodeIndex]->GetJoint(); - if (joint) + + if (AzPhysics::Joint* joint = m_nodes[nodeIndex]->GetJoint()) { if (physx::PxD6Joint* pxJoint = static_cast(joint->GetNativePointer())) { diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp index 0f9c7644cd..a76fecb613 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp @@ -38,11 +38,17 @@ namespace PhysX RagdollNode::~RagdollNode() { + DestroyJoint(); DestroyPhysicsBody(); } - void RagdollNode::SetJoint(const AZStd::shared_ptr& joint) + void RagdollNode::SetJoint(AzPhysics::Joint* joint) { + if (m_joint) + { + return; + } + m_joint = joint; } @@ -52,7 +58,7 @@ namespace PhysX return *m_rigidBody; } - const AZStd::shared_ptr& RagdollNode::GetJoint() const + AzPhysics::Joint* RagdollNode::GetJoint() { return m_joint; } @@ -167,4 +173,16 @@ namespace PhysX } } + void RagdollNode::DestroyJoint() + { + if (m_joint != nullptr && m_sceneOwner != AzPhysics::InvalidSceneHandle) + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->RemoveJoint(m_sceneOwner, m_joint->m_jointHandle); + } + m_joint = nullptr; + } + } + } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.h b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.h index 0567723c01..2f1e508dd4 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.h +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.h @@ -32,11 +32,11 @@ namespace PhysX explicit RagdollNode(AzPhysics::SceneHandle sceneHandle, Physics::RagdollNodeConfiguration& nodeConfig); ~RagdollNode(); - void SetJoint(const AZStd::shared_ptr& joint); + void SetJoint(AzPhysics::Joint* joint); // Physics::RagdollNode AzPhysics::RigidBody& GetRigidBody() override; - const AZStd::shared_ptr& GetJoint() const override; + AzPhysics::Joint* GetJoint() override; bool IsSimulating() const override; // AzPhysics::SimulatedBody @@ -60,9 +60,10 @@ namespace PhysX private: void CreatePhysicsBody(AzPhysics::SceneHandle sceneHandle, Physics::RagdollNodeConfiguration& nodeConfig); void DestroyPhysicsBody(); + void DestroyJoint(); - AZStd::shared_ptr m_joint; - AzPhysics::RigidBody* m_rigidBody; + AzPhysics::Joint* m_joint = nullptr; + AzPhysics::RigidBody* m_rigidBody = nullptr; AzPhysics::SimulatedBodyHandle m_rigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle; AzPhysics::SceneHandle m_sceneOwner = AzPhysics::InvalidSceneHandle; PhysX::ActorData m_actorUserData; diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/Components/RagdollComponent.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/Components/RagdollComponent.cpp index 8da512647f..c341fbf348 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/Components/RagdollComponent.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/Components/RagdollComponent.cpp @@ -379,7 +379,7 @@ namespace PhysX for (size_t nodeIndex = 0; nodeIndex < numNodes; nodeIndex++) { - if (const AZStd::shared_ptr& joint = ragdoll->GetNode(nodeIndex)->GetJoint()) + if (const AzPhysics::Joint* joint = ragdoll->GetNode(nodeIndex)->GetJoint()) { if (auto* pxJoint = static_cast(joint->GetNativePointer())) { diff --git a/Gems/PhysX/Code/Source/Platform/Android/PAL_android.cmake b/Gems/PhysX/Code/Source/Platform/Android/PAL_android.cmake index 975225b8a4..4002fe5484 100644 --- a/Gems/PhysX/Code/Source/Platform/Android/PAL_android.cmake +++ b/Gems/PhysX/Code/Source/Platform/Android/PAL_android.cmake @@ -10,3 +10,5 @@ # set(PAL_TRAIT_PHYSX_SUPPORTED TRUE) +set(PAL_TRAIT_JOINTS_TYPED_TEST_CASE FALSE) + diff --git a/Gems/PhysX/Code/Source/Platform/Linux/PAL_linux.cmake b/Gems/PhysX/Code/Source/Platform/Linux/PAL_linux.cmake index 975225b8a4..d57b460d13 100644 --- a/Gems/PhysX/Code/Source/Platform/Linux/PAL_linux.cmake +++ b/Gems/PhysX/Code/Source/Platform/Linux/PAL_linux.cmake @@ -10,3 +10,4 @@ # set(PAL_TRAIT_PHYSX_SUPPORTED TRUE) +set(PAL_TRAIT_JOINTS_TYPED_TEST_CASE FALSE) diff --git a/Gems/PhysX/Code/Source/Platform/Mac/PAL_mac.cmake b/Gems/PhysX/Code/Source/Platform/Mac/PAL_mac.cmake index 975225b8a4..052ad091e4 100644 --- a/Gems/PhysX/Code/Source/Platform/Mac/PAL_mac.cmake +++ b/Gems/PhysX/Code/Source/Platform/Mac/PAL_mac.cmake @@ -10,3 +10,4 @@ # set(PAL_TRAIT_PHYSX_SUPPORTED TRUE) +set(PAL_TRAIT_JOINTS_TYPED_TEST_CASE TRUE) \ No newline at end of file diff --git a/Gems/PhysX/Code/Source/Platform/Windows/PAL_windows.cmake b/Gems/PhysX/Code/Source/Platform/Windows/PAL_windows.cmake index 975225b8a4..052ad091e4 100644 --- a/Gems/PhysX/Code/Source/Platform/Windows/PAL_windows.cmake +++ b/Gems/PhysX/Code/Source/Platform/Windows/PAL_windows.cmake @@ -10,3 +10,4 @@ # set(PAL_TRAIT_PHYSX_SUPPORTED TRUE) +set(PAL_TRAIT_JOINTS_TYPED_TEST_CASE TRUE) \ No newline at end of file diff --git a/Gems/PhysX/Code/Source/Platform/iOS/PAL_ios.cmake b/Gems/PhysX/Code/Source/Platform/iOS/PAL_ios.cmake index 975225b8a4..b7794acee0 100644 --- a/Gems/PhysX/Code/Source/Platform/iOS/PAL_ios.cmake +++ b/Gems/PhysX/Code/Source/Platform/iOS/PAL_ios.cmake @@ -10,3 +10,4 @@ # set(PAL_TRAIT_PHYSX_SUPPORTED TRUE) +set(PAL_TRAIT_JOINTS_TYPED_TEST_CASE TRUE) diff --git a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp index 689ea47be7..a47e2a99ec 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp +++ b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include namespace PhysX { @@ -232,6 +234,18 @@ namespace PhysX scene->GetSceneHandle()); } + template + AzPhysics::Joint* CreateJoint(const ConfigurationType* configuration, + AzPhysics::SceneHandle sceneHandle, + AzPhysics::SimulatedBodyHandle parentBodyHandle, + AzPhysics::SimulatedBodyHandle childBodyHandle, + AZ::Crc32& crc) + { + JointType* newBody = aznew JointType(*configuration, sceneHandle, parentBodyHandle, childBodyHandle); + crc = AZ::Crc32(newBody, sizeof(*newBody)); + return newBody; + } + //helper to perform a ray cast AzPhysics::SceneQueryHits RayCast(const AzPhysics::RayCastRequest* raycastRequest, AZStd::vector& raycastBuffer, @@ -813,6 +827,90 @@ namespace PhysX } } + AzPhysics::JointHandle PhysXScene::AddJoint(const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody) + { + AzPhysics::Joint* newJoint = nullptr; + AZ::Crc32 newJointCrc; + if (azrtti_istypeof(jointConfig)) + { + newJoint = Internal::CreateJoint( + azdynamic_cast(jointConfig), + m_sceneHandle, parentBody, childBody, newJointCrc); + } + else if (azrtti_istypeof(jointConfig)) + { + newJoint = Internal::CreateJoint( + azdynamic_cast(jointConfig), + m_sceneHandle, parentBody, childBody, newJointCrc); + } + else if (azrtti_istypeof(jointConfig)) + { + newJoint = Internal::CreateJoint( + azdynamic_cast(jointConfig), + m_sceneHandle, parentBody, childBody, newJointCrc); + } + else if (azrtti_istypeof(jointConfig)) + { + newJoint = Internal::CreateJoint( + azdynamic_cast(jointConfig), + m_sceneHandle, parentBody, childBody, newJointCrc); + } + else + { + AZ_Warning("PhysXScene", false, "Unknown JointConfiguration."); + return AzPhysics::InvalidJointHandle; + } + + if (newJoint != nullptr) + { + AzPhysics::JointIndex index = index = m_joints.size(); + m_joints.emplace_back(newJointCrc, newJoint); + + const AzPhysics::JointHandle newJointHandle(newJointCrc, index); + newJoint->m_sceneOwner = m_sceneHandle; + newJoint->m_jointHandle = newJointHandle; + + return newJointHandle; + } + + return AzPhysics::InvalidJointHandle; + } + + AzPhysics::Joint* PhysXScene::GetJointFromHandle(AzPhysics::JointHandle jointHandle) + { + if (jointHandle == AzPhysics::InvalidJointHandle) + { + return nullptr; + } + + AzPhysics::JointIndex index = AZStd::get(jointHandle); + if (index < m_joints.size() + && m_joints[index].first == AZStd::get(jointHandle)) + { + return m_joints[index].second; + } + return nullptr; + } + + void PhysXScene::RemoveJoint(AzPhysics::JointHandle jointHandle) + { + if (jointHandle == AzPhysics::InvalidJointHandle) + { + return; + } + + AzPhysics::JointIndex index = AZStd::get(jointHandle); + if (index < m_joints.size() + && m_joints[index].first == AZStd::get(jointHandle)) + { + m_deferredDeletionsJoints.push_back(m_joints[index].second); + m_joints[index] = AZStd::make_pair(AZ::Crc32(), nullptr); + m_freeJointSlots.push(index); + jointHandle = AzPhysics::InvalidJointHandle; + } + } + AzPhysics::SceneQueryHits PhysXScene::QueryScene(const AzPhysics::SceneQueryRequest* request) { if (request == nullptr) @@ -996,6 +1094,13 @@ namespace PhysX { delete simulatedBody; } + + AZStd::vector jointDeletions; + jointDeletions.swap(m_deferredDeletionsJoints); + for (auto* joint : jointDeletions) + { + delete joint; + } } void PhysXScene::ProcessTriggerEvents() diff --git a/Gems/PhysX/Code/Source/Scene/PhysXScene.h b/Gems/PhysX/Code/Source/Scene/PhysXScene.h index 2e257283f0..c6bb045e65 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXScene.h +++ b/Gems/PhysX/Code/Source/Scene/PhysXScene.h @@ -12,6 +12,7 @@ #pragma once #include +#include #include #include #include @@ -52,6 +53,10 @@ namespace PhysX void RemoveSimulatedBodies(AzPhysics::SimulatedBodyHandleList& bodyHandles) override; void EnableSimulationOfBody(AzPhysics::SimulatedBodyHandle bodyHandle) override; void DisableSimulationOfBody(AzPhysics::SimulatedBodyHandle bodyHandle) override; + AzPhysics::JointHandle AddJoint(const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody) override; + AzPhysics::Joint* GetJointFromHandle(AzPhysics::JointHandle jointHandle) override; + void RemoveJoint(AzPhysics::JointHandle jointHandle) override; AzPhysics::SceneQueryHits QueryScene(const AzPhysics::SceneQueryRequest* request) override; AzPhysics::SceneQueryHitsList QuerySceneBatch(const AzPhysics::SceneQueryRequests& requests) override; [[nodiscard]] bool QuerySceneAsync(AzPhysics::SceneQuery::AsyncRequestId requestId, @@ -94,6 +99,10 @@ namespace PhysX AZStd::vector m_deferredDeletions; AZStd::queue m_freeSceneSlots; + AZStd::vector> m_joints; + AZStd::vector m_deferredDeletionsJoints; + AZStd::queue m_freeJointSlots; + AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physicsSystemConfigChanged; static thread_local AZStd::vector s_rayCastBuffer; //!< thread local structure to hold hits for a single raycast or shapecast. diff --git a/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.cpp b/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.cpp index 3b3ab2f0f8..9aeafb1931 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.cpp +++ b/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.cpp @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -144,6 +145,36 @@ namespace PhysX } } + AzPhysics::JointHandle PhysXSceneInterface::AddJoint( + AzPhysics::SceneHandle sceneHandle, const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody) + { + if (AzPhysics::Scene* scene = m_physxSystem->GetScene(sceneHandle)) + { + return scene->AddJoint(jointConfig, parentBody, childBody); + } + + return AzPhysics::InvalidJointHandle; + } + + AzPhysics::Joint* PhysXSceneInterface::GetJointFromHandle(AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle jointHandle) + { + if (AzPhysics::Scene* scene = m_physxSystem->GetScene(sceneHandle)) + { + return scene->GetJointFromHandle(jointHandle); + } + + return nullptr; + } + + void PhysXSceneInterface::RemoveJoint(AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle jointHandle) + { + if (AzPhysics::Scene* scene = m_physxSystem->GetScene(sceneHandle)) + { + scene->RemoveJoint(jointHandle); + } + } + AzPhysics::SceneQueryHits PhysXSceneInterface::QueryScene( AzPhysics::SceneHandle sceneHandle, const AzPhysics::SceneQueryRequest* request) { diff --git a/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.h b/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.h index 2edfbd8457..b96f74a3f7 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.h +++ b/Gems/PhysX/Code/Source/Scene/PhysXSceneInterface.h @@ -44,6 +44,10 @@ namespace PhysX void RemoveSimulatedBodies(AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandleList& bodyHandles) override; void EnableSimulationOfBody(AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle) override; void DisableSimulationOfBody(AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle) override; + AzPhysics::JointHandle AddJoint(AzPhysics::SceneHandle sceneHandle, const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody) override; + AzPhysics::Joint* GetJointFromHandle(AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle jointHandle) override; + void RemoveJoint(AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle jointHandle) override; AzPhysics::SceneQueryHits QueryScene(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SceneQueryRequest* request) override; AzPhysics::SceneQueryHitsList QuerySceneBatch(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SceneQueryRequests& requests) override; [[nodiscard]] bool QuerySceneAsync(AzPhysics::SceneHandle sceneHandle, AzPhysics::SceneQuery::AsyncRequestId requestId, diff --git a/Gems/PhysX/Code/Source/System/PhysXJointInterface.cpp b/Gems/PhysX/Code/Source/System/PhysXJointInterface.cpp new file mode 100644 index 0000000000..1fa13cffb4 --- /dev/null +++ b/Gems/PhysX/Code/Source/System/PhysXJointInterface.cpp @@ -0,0 +1,299 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates, or + * a third party where indicated. + * + * 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 PhysX +{ + namespace + { + struct D6JointState + { + float m_swingAngleY; + float m_swingAngleZ; + float m_twistAngle; + }; + + D6JointState CalculateD6JointState( + const AZ::Quaternion& parentWorldRotation, + const AZ::Quaternion& parentLocalRotation, + const AZ::Quaternion& childWorldRotation, + const AZ::Quaternion& childLocalRotation) + { + D6JointState result; + + const AZ::Quaternion parentRotation = parentWorldRotation * parentLocalRotation; + const AZ::Quaternion childRotation = childWorldRotation * childLocalRotation; + const AZ::Quaternion relativeRotation = parentRotation.GetConjugate() * childRotation; + AZ::Quaternion twistQuat = AZ::IsClose(relativeRotation.GetX(), 0.0f, AZ::Constants::FloatEpsilon) + ? AZ::Quaternion::CreateIdentity() + : AZ::Quaternion(relativeRotation.GetX(), 0.0f, 0.0f, relativeRotation.GetW()).GetNormalized(); + AZ::Quaternion swingQuat = relativeRotation * twistQuat.GetConjugate(); + + // make sure the twist angle has the correct sign for the rotation + twistQuat *= AZ::GetSign(twistQuat.GetX()); + // make sure we get the shortest arcs for the swing degrees of freedom + swingQuat *= AZ::GetSign(swingQuat.GetW()); + // the PhysX swing limits work in terms of tan quarter angles + result.m_swingAngleY = 4.0f * atan2f(swingQuat.GetY(), 1.0f + swingQuat.GetW()); + result.m_swingAngleZ = 4.0f * atan2f(swingQuat.GetZ(), 1.0f + swingQuat.GetW()); + const float twistAngle = twistQuat.GetAngle(); + // GetAngle returns an angle in the range 0..2 pi, but the twist limits work in the range -pi..pi + const float wrappedTwistAngle = twistAngle > AZ::Constants::Pi ? twistAngle - AZ::Constants::TwoPi : twistAngle; + result.m_twistAngle = wrappedTwistAngle; + + return result; + } + + bool IsD6SwingValid(float swingAngleY, float swingAngleZ, float swingLimitY, float swingLimitZ) + { + const float epsilon = AZ::Constants::FloatEpsilon; + const float yFactor = tanf(0.25f * swingAngleY) / AZStd::GetMax(epsilon, tanf(0.25f * swingLimitY)); + const float zFactor = tanf(0.25f * swingAngleZ) / AZStd::GetMax(epsilon, tanf(0.25f * swingLimitZ)); + + return (yFactor * yFactor + zFactor * zFactor <= 1.0f + epsilon); + } + + void AppendD6SwingConeToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float swingAngleY, + float swingAngleZ, + float swingLimitY, + float swingLimitZ, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::u32 numLinesSwingCone = angularSubdivisions * (1u + radialSubdivisions); + lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesSwingCone); + lineValidityBufferOut.reserve(lineValidityBufferOut.size() + numLinesSwingCone); + + // the orientation quat for a radial line in the cone can be represented in terms of sin and cos half angles + // these expressions can be efficiently calculated using tan quarter angles as follows: + // writing t = tan(x / 4) + // sin(x / 2) = 2 * t / (1 + t * t) + // cos(x / 2) = (1 - t * t) / (1 + t * t) + const float tanQuarterSwingZ = tanf(0.25f * swingLimitZ); + const float tanQuarterSwingY = tanf(0.25f * swingLimitY); + + AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); + for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) + { + const float angle = AZ::Constants::TwoPi / angularSubdivisions * angularIndex; + // the axis about which to rotate the x-axis to get the radial vector for this segment of the cone + const AZ::Vector3 rotationAxis(0, -tanQuarterSwingY * sinf(angle), tanQuarterSwingZ * cosf(angle)); + const float normalizationFactor = rotationAxis.GetLengthSq(); + const AZ::Quaternion radialVectorRotation = 1.0f / (1.0f + normalizationFactor) * + AZ::Quaternion::CreateFromVector3AndValue(2.0f * rotationAxis, 1.0f - normalizationFactor); + const AZ::Vector3 radialVector = + (parentLocalRotation * radialVectorRotation).TransformVector(AZ::Vector3::CreateAxisX(scale)); + + if (angularIndex > 0) + { + for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) + { + float radiusFraction = 1.0f / radialSubdivisions * radialIndex; + lineBufferOut.push_back(radiusFraction * radialVector); + lineBufferOut.push_back(radiusFraction * previousRadialVector); + } + } + + if (angularIndex < angularSubdivisions) + { + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(radialVector); + } + + previousRadialVector = radialVector; + } + + const bool swingValid = IsD6SwingValid(swingAngleY, swingAngleZ, swingLimitY, swingLimitZ); + lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesSwingCone, swingValid); + } + + void AppendD6TwistArcToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + float twistLimitLower, + float twistLimitUpper, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::u32 numLinesTwistArc = angularSubdivisions * (1u + radialSubdivisions) + 1u; + lineBufferOut.reserve(lineBufferOut.size() + 2u * numLinesTwistArc); + + AZ::Vector3 previousRadialVector = AZ::Vector3::CreateZero(); + const float twistRange = twistLimitUpper - twistLimitLower; + + for (AZ::u32 angularIndex = 0; angularIndex <= angularSubdivisions; angularIndex++) + { + const float angle = twistLimitLower + twistRange / angularSubdivisions * angularIndex; + const AZ::Vector3 radialVector = parentLocalRotation.TransformVector(scale * AZ::Vector3(0.0f, cosf(angle), sinf(angle))); + + if (angularIndex > 0) + { + for (AZ::u32 radialIndex = 1; radialIndex <= radialSubdivisions; radialIndex++) + { + const float radiusFraction = 1.0f / radialSubdivisions * radialIndex; + lineBufferOut.push_back(radiusFraction * radialVector); + lineBufferOut.push_back(radiusFraction * previousRadialVector); + } + } + + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(radialVector); + + previousRadialVector = radialVector; + } + + const bool twistValid = (twistAngle >= twistLimitLower && twistAngle <= twistLimitUpper); + lineValidityBufferOut.insert(lineValidityBufferOut.end(), numLinesTwistArc, twistValid); + } + + void AppendD6CurrentTwistToLineBuffer( + const AZ::Quaternion& parentLocalRotation, + float twistAngle, + [[maybe_unused]] float twistLimitLower, + [[maybe_unused]] float twistLimitUpper, + float scale, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + const AZ::Vector3 twistVector = + parentLocalRotation.TransformVector(1.25f * scale * AZ::Vector3(0.0f, cosf(twistAngle), sinf(twistAngle))); + lineBufferOut.push_back(AZ::Vector3::CreateZero()); + lineBufferOut.push_back(twistVector); + lineValidityBufferOut.push_back(true); + } + + template + AZStd::unique_ptr ConfigurationFactory( + const AZ::Quaternion& parentLocalRotation, const AZ::Quaternion& childLocalRotation) + { + auto jointConfig = AZStd::make_unique(); + jointConfig->m_childLocalRotation = childLocalRotation; + jointConfig->m_parentLocalRotation = parentLocalRotation; + + return jointConfig; + } + + } // namespace + + const AZStd::vector PhysXJointHelpersInterface::GetSupportedJointTypeIds() const + { + static AZStd::vector jointTypes = { + D6JointLimitConfiguration::RTTI_Type(), + FixedJointConfiguration::RTTI_Type(), + BallJointConfiguration::RTTI_Type(), + HingeJointConfiguration::RTTI_Type() + }; + return jointTypes; + } + + AZStd::optional PhysXJointHelpersInterface::GetSupportedJointTypeId(AzPhysics::JointType typeEnum) const + { + switch (typeEnum) + { + case AzPhysics::JointType::D6Joint: + return azrtti_typeid(); + case AzPhysics::JointType::FixedJoint: + return azrtti_typeid(); + case AzPhysics::JointType::BallJoint: + return azrtti_typeid(); + case AzPhysics::JointType::HingeJoint: + return azrtti_typeid(); + default: + AZ_Warning("PhysX Joint Utils", false, "Unsupported joint type in GetSupportedJointTypeId"); + } + return AZStd::nullopt; + } + + AZStd::unique_ptr PhysXJointHelpersInterface::ComputeInitialJointLimitConfiguration( + const AZ::TypeId& jointLimitTypeId, + const AZ::Quaternion& parentWorldRotation, + const AZ::Quaternion& childWorldRotation, + const AZ::Vector3& axis, + [[maybe_unused]] const AZStd::vector& exampleLocalRotations) + { + const AZ::Vector3& normalizedAxis = axis.IsZero() ? AZ::Vector3::CreateAxisX() : axis.GetNormalized(); + const AZ::Quaternion childLocalRotation = AZ::Quaternion::CreateShortestArc( + AZ::Vector3::CreateAxisX(), childWorldRotation.GetConjugate().TransformVector(normalizedAxis)); + const AZ::Quaternion parentLocalRotation = parentWorldRotation.GetConjugate() * childWorldRotation * childLocalRotation; + + if (jointLimitTypeId == azrtti_typeid()) + { + return ConfigurationFactory(parentLocalRotation, childLocalRotation); + } + else if (jointLimitTypeId == azrtti_typeid()) + { + return ConfigurationFactory(parentLocalRotation, childLocalRotation); + } + else if (jointLimitTypeId == azrtti_typeid()) + { + return ConfigurationFactory(parentLocalRotation, childLocalRotation); + } + else if (jointLimitTypeId == azrtti_typeid()) + { + return ConfigurationFactory(parentLocalRotation, childLocalRotation); + } + + AZ_Warning("PhysX Joint Utils", false, "Unsupported joint type in ComputeInitialJointLimitConfiguration"); + return nullptr; + } + + void PhysXJointHelpersInterface::GenerateJointLimitVisualizationData( + const AzPhysics::JointConfiguration& configuration, + const AZ::Quaternion& parentRotation, + const AZ::Quaternion& childRotation, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + [[maybe_unused]] AZStd::vector& vertexBufferOut, + [[maybe_unused]] AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) + { + if (const auto d6JointConfiguration = azrtti_cast(&configuration)) + { + const AZ::u32 angularSubdivisionsClamped = AZ::GetClamp(angularSubdivisions, 4u, 32u); + const AZ::u32 radialSubdivisionsClamped = AZ::GetClamp(radialSubdivisions, 1u, 4u); + + const D6JointState jointState = CalculateD6JointState( + parentRotation, d6JointConfiguration->m_parentLocalRotation, childRotation, d6JointConfiguration->m_childLocalRotation); + const float swingAngleY = jointState.m_swingAngleY; + const float swingAngleZ = jointState.m_swingAngleZ; + const float twistAngle = jointState.m_twistAngle; + const float swingLimitY = AZ::DegToRad(d6JointConfiguration->m_swingLimitY); + const float swingLimitZ = AZ::DegToRad(d6JointConfiguration->m_swingLimitZ); + const float twistLimitLower = AZ::DegToRad(d6JointConfiguration->m_twistLimitLower); + const float twistLimitUpper = AZ::DegToRad(d6JointConfiguration->m_twistLimitUpper); + + AppendD6SwingConeToLineBuffer( + d6JointConfiguration->m_parentLocalRotation, swingAngleY, swingAngleZ, swingLimitY, swingLimitZ, scale, + angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); + AppendD6TwistArcToLineBuffer( + d6JointConfiguration->m_parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, scale, + angularSubdivisionsClamped, radialSubdivisionsClamped, lineBufferOut, lineValidityBufferOut); + AppendD6CurrentTwistToLineBuffer( + d6JointConfiguration->m_parentLocalRotation, twistAngle, twistLimitLower, twistLimitUpper, scale, lineBufferOut, + lineValidityBufferOut); + } + } +} diff --git a/Gems/PhysX/Code/Source/System/PhysXJointInterface.h b/Gems/PhysX/Code/Source/System/PhysXJointInterface.h new file mode 100644 index 0000000000..1cee865e8b --- /dev/null +++ b/Gems/PhysX/Code/Source/System/PhysXJointInterface.h @@ -0,0 +1,52 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates, or + * a third party where indicated. + * + * 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 + +namespace AzPhysics +{ + struct JointConfiguration; +} + +namespace PhysX +{ + class PhysXJointHelpersInterface + : public AZ::Interface::Registrar + { + public: + AZ_RTTI(PhysX::PhysXJointHelpersInterface, "{48AC5137-2226-4C57-8E4C-FCF3C1965252}", AzPhysics::JointHelpersInterface); + + const AZStd::vector GetSupportedJointTypeIds() const override; + AZStd::optional GetSupportedJointTypeId(AzPhysics::JointType typeEnum) const override; + + AZStd::unique_ptr ComputeInitialJointLimitConfiguration( + const AZ::TypeId& jointLimitTypeId, + const AZ::Quaternion& parentWorldRotation, + const AZ::Quaternion& childWorldRotation, + const AZ::Vector3& axis, + const AZStd::vector& exampleLocalRotations) override; + + void GenerateJointLimitVisualizationData( + const AzPhysics::JointConfiguration& configuration, + const AZ::Quaternion& parentRotation, + const AZ::Quaternion& childRotation, + float scale, + AZ::u32 angularSubdivisions, + AZ::u32 radialSubdivisions, + AZStd::vector& vertexBufferOut, + AZStd::vector& indexBufferOut, + AZStd::vector& lineBufferOut, + AZStd::vector& lineValidityBufferOut) override; + }; +} diff --git a/Gems/PhysX/Code/Source/System/PhysXSystem.h b/Gems/PhysX/Code/Source/System/PhysXSystem.h index 533685bbd1..60f72c6145 100644 --- a/Gems/PhysX/Code/Source/System/PhysXSystem.h +++ b/Gems/PhysX/Code/Source/System/PhysXSystem.h @@ -25,6 +25,7 @@ #include #include +#include namespace physx { @@ -128,6 +129,7 @@ namespace PhysX Debug::PhysXDebug m_physXDebug; //! Handler for the PhysXDebug Interface. PhysXSettingsRegistryManager& m_registryManager; //! Handles all settings registry interactions. PhysXSceneInterface m_sceneInterface; //! Implemented the Scene Az::Interface. + PhysXJointHelpersInterface m_jointHelperInterface; //! Implementation of the JointHelpersInterface. class MaterialLibraryAssetHelper : private AZ::Data::AssetBus::Handler diff --git a/Gems/PhysX/Code/Source/SystemComponent.cpp b/Gems/PhysX/Code/Source/SystemComponent.cpp index eae56967b2..6dcf3951f4 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.cpp +++ b/Gems/PhysX/Code/Source/SystemComponent.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -93,7 +92,6 @@ namespace PhysX void SystemComponent::Reflect(AZ::ReflectContext* context) { - D6JointLimitConfiguration::Reflect(context); Pipeline::MeshAsset::Reflect(context); PhysX::ReflectionUtils::ReflectPhysXOnlyApi(context); @@ -347,49 +345,6 @@ namespace PhysX return AZStd::make_shared(materialConfiguration); } - AZStd::vector SystemComponent::GetSupportedJointTypes() - { - return JointUtils::GetSupportedJointTypes(); - } - - AZStd::shared_ptr SystemComponent::CreateJointLimitConfiguration(AZ::TypeId jointType) - { - return JointUtils::CreateJointLimitConfiguration(jointType); - } - - AZStd::shared_ptr SystemComponent::CreateJoint(const AZStd::shared_ptr& configuration, - AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody) - { - return JointUtils::CreateJoint(configuration, parentBody, childBody); - } - - void SystemComponent::GenerateJointLimitVisualizationData( - const Physics::JointLimitConfiguration& configuration, - const AZ::Quaternion& parentRotation, - const AZ::Quaternion& childRotation, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) - { - JointUtils::GenerateJointLimitVisualizationData(configuration, parentRotation, childRotation, scale, - angularSubdivisions, radialSubdivisions, vertexBufferOut, indexBufferOut, lineBufferOut, lineValidityBufferOut); - } - - AZStd::unique_ptr SystemComponent::ComputeInitialJointLimitConfiguration( - const AZ::TypeId& jointLimitTypeId, - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Vector3& axis, - const AZStd::vector& exampleLocalRotations) - { - return JointUtils::ComputeInitialJointLimitConfiguration(jointLimitTypeId, parentWorldRotation, - childWorldRotation, axis, exampleLocalRotations); - } - void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject) { if (nativeMeshObject) diff --git a/Gems/PhysX/Code/Source/SystemComponent.h b/Gems/PhysX/Code/Source/SystemComponent.h index 4609fde0ce..aab5c5a8e9 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.h +++ b/Gems/PhysX/Code/Source/SystemComponent.h @@ -115,28 +115,6 @@ namespace PhysX AZStd::shared_ptr CreateShape(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration) override; AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override; - AZStd::vector GetSupportedJointTypes() override; - AZStd::shared_ptr CreateJointLimitConfiguration(AZ::TypeId jointType) override; - AZStd::shared_ptr CreateJoint(const AZStd::shared_ptr& configuration, - AzPhysics::SimulatedBody* parentBody, AzPhysics::SimulatedBody* childBody) override; - void GenerateJointLimitVisualizationData( - const Physics::JointLimitConfiguration& configuration, - const AZ::Quaternion& parentRotation, - const AZ::Quaternion& childRotation, - float scale, - AZ::u32 angularSubdivisions, - AZ::u32 radialSubdivisions, - AZStd::vector& vertexBufferOut, - AZStd::vector& indexBufferOut, - AZStd::vector& lineBufferOut, - AZStd::vector& lineValidityBufferOut) override; - AZStd::unique_ptr ComputeInitialJointLimitConfiguration( - const AZ::TypeId& jointLimitTypeId, - const AZ::Quaternion& parentWorldRotation, - const AZ::Quaternion& childWorldRotation, - const AZ::Vector3& axis, - const AZStd::vector& exampleLocalRotations) override; - void ReleaseNativeMeshObject(void* nativeMeshObject) override; // Assets related data diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index 270a352fc0..cb28358213 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -40,9 +40,9 @@ #include #include #include -#include #include #include +#include namespace PhysX { @@ -1394,8 +1394,12 @@ namespace PhysX ForceRegionBusBehaviorHandler::Reflect(context); - GenericJointConfiguration::Reflect(context); - GenericJointLimitsConfiguration::Reflect(context); + D6JointLimitConfiguration::Reflect(context); + JointGenericProperties::Reflect(context); + JointLimitProperties::Reflect(context); + FixedJointConfiguration::Reflect(context); + BallJointConfiguration::Reflect(context); + HingeJointConfiguration::Reflect(context); } void ForceRegionBusBehaviorHandler::Reflect(AZ::ReflectContext* context) diff --git a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksUtilities.h b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksUtilities.h index 0971226644..5a54ab5833 100644 --- a/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksUtilities.h +++ b/Gems/PhysX/Code/Tests/Benchmarks/PhysXBenchmarksUtilities.h @@ -24,6 +24,7 @@ namespace AzPhysics { class Scene; + struct RigidBody; } namespace PhysX::Benchmarks diff --git a/Gems/PhysX/Code/Tests/Benchmarks/PhysXJointBenchmarks.cpp b/Gems/PhysX/Code/Tests/Benchmarks/PhysXJointBenchmarks.cpp index 24b51f0085..e1ebed18ee 100644 --- a/Gems/PhysX/Code/Tests/Benchmarks/PhysXJointBenchmarks.cpp +++ b/Gems/PhysX/Code/Tests/Benchmarks/PhysXJointBenchmarks.cpp @@ -26,7 +26,7 @@ #include #include -#include +#include namespace PhysX::Benchmarks { @@ -102,7 +102,7 @@ namespace PhysX::Benchmarks { AzPhysics::RigidBody* m_parent; AzPhysics::SimulatedBody* m_child; - AZStd::shared_ptr m_joint; + AzPhysics::JointHandle m_jointHandle; }; //! Structure to hold the upper and lower twist limits //! used with Utils::CreateJoints GenerateTwistLimitsFuncPtr @@ -122,13 +122,11 @@ namespace PhysX::Benchmarks //! Helper function to add the required number of joints to the provided world //! @param numJoints, the requested number of joints to spawn - //! @param system, current active physics system //! @param scene, the physics scene to spawn the joints and their world bodies into //! @param parentPositionGenerator, [optional] function pointer to allow caller to pick the spawn position of the parent body //! @param childPositionGenerator, [optional] function pointer to allow caller to pick the spawn position of the child body //! @param GenerateTwistLimitsFuncPtr, [optional] function pointer to allow caller to pick the twist limits of the joint AZStd::vector CreateJoints(int numJoints, - Physics::System* system, AzPhysics::Scene* scene, GenerateSpawnPositionFuncPtr* parentPositionGenerator = nullptr, GenerateSpawnPositionFuncPtr* childPositionGenerator = nullptr, @@ -173,18 +171,18 @@ namespace PhysX::Benchmarks AzPhysics::SimulatedBodyHandle staticRigidBodyHandle = scene->AddSimulatedBody(&staticRigidBodyConfig); newJoint.m_child = scene->GetSimulatedBodyFromHandle(staticRigidBodyHandle); - AZStd::shared_ptr config = AZStd::make_shared(); + PhysX::D6JointLimitConfiguration config; TwistLimits limits(JointConstants::CreateJointDefaults::UpperLimit, JointConstants::CreateJointDefaults::LowerLimit); if (twistLimitsGenerator) { limits = (*twistLimitsGenerator)(i); } - config->m_twistLimitUpper = limits.m_upperLimit; - config->m_twistLimitLower = limits.m_lowerLimit; - config->m_swingLimitY = 1.0f; - config->m_swingLimitZ = 1.0f; - newJoint.m_joint = system->CreateJoint(config, newJoint.m_parent, newJoint.m_child); + config.m_twistLimitUpper = limits.m_upperLimit; + config.m_twistLimitLower = limits.m_lowerLimit; + config.m_swingLimitY = 1.0f; + config.m_swingLimitZ = 1.0f; + newJoint.m_jointHandle = scene->AddJoint(&config, newJoint.m_parent->m_bodyHandle, newJoint.m_child->m_bodyHandle); joints.emplace_back(AZStd::move(newJoint)); } @@ -201,8 +199,6 @@ namespace PhysX::Benchmarks virtual void SetUp([[maybe_unused]] const ::benchmark::State &state) override { PhysXBaseBenchmarkFixture::SetUpInternal(); - //need to get the Physics::System to be able to spawn the rigid bodies - m_system = AZ::Interface::Get(); } virtual void TearDown([[maybe_unused]] const ::benchmark::State &state) override @@ -218,8 +214,6 @@ namespace PhysX::Benchmarks return sceneConfig; } // PhysXBaseBenchmarkFixture Interface --------- - - Physics::System *m_system; }; //! BM_Joints_AtRest - This test will spawn the requested number of joints @@ -237,7 +231,7 @@ namespace PhysX::Benchmarks return position; }; - AZStd::vector joinGroups = Utils::CreateJoints(aznumeric_cast(state.range(0)), m_system, m_defaultScene, + AZStd::vector joinGroups = Utils::CreateJoints(aznumeric_cast(state.range(0)), m_defaultScene, &parentPosGenerator, &childPosGenerator); //setup the sub tick tracker @@ -260,6 +254,11 @@ namespace PhysX::Benchmarks } subTickTracker.Stop(); + for (const auto& jointGroup : joinGroups) + { + m_defaultScene->RemoveJoint(jointGroup.m_jointHandle); + } + //sort the frame times and get the P50, P90, P99 percentiles Utils::ReportFramePercentileCounters(state, tickTimes, subTickTracker.GetSubTickTimes()); Utils::ReportFrameStandardDeviationAndMeanCounters(state, tickTimes, subTickTracker.GetSubTickTimes()); @@ -287,7 +286,7 @@ namespace PhysX::Benchmarks return Utils::TwistLimits(JointConstants::JointSettings::SwingingJointUpperLimit, JointConstants::JointSettings::SwingingJointLowerLimit); }; - AZStd::vector joinGroups = Utils::CreateJoints(aznumeric_cast(state.range(0)), m_system, m_defaultScene, + AZStd::vector joinGroups = Utils::CreateJoints(aznumeric_cast(state.range(0)), m_defaultScene, &parentPosGenerator, &childPosGenerator, &twistGenerator); //setup the sub tick tracker @@ -326,6 +325,11 @@ namespace PhysX::Benchmarks } subTickTracker.Stop(); + for (const auto& jointGroup : joinGroups) + { + m_defaultScene->RemoveJoint(jointGroup.m_jointHandle); + } + //sort the frame times and get the P50, P90, P99 percentiles Utils::ReportFramePercentileCounters(state, tickTimes, subTickTracker.GetSubTickTimes()); Utils::ReportFrameStandardDeviationAndMeanCounters(state, tickTimes, subTickTracker.GetSubTickTimes()); @@ -370,17 +374,18 @@ namespace PhysX::Benchmarks snakeRigidBodies = Utils::GetRigidBodiesFromHandles(m_defaultScene, snakeRigidBodyHandles); //build the snake - AZStd::vector> joints; - AZStd::shared_ptr config = AZStd::make_shared(); - config->m_twistLimitUpper = JointConstants::CreateJointDefaults::UpperLimit; - config->m_twistLimitLower = JointConstants::CreateJointDefaults::LowerLimit; - config->m_swingLimitY = 1.0f; - config->m_swingLimitZ = 1.0f; + AZStd::vector jointHandles; + PhysX::D6JointLimitConfiguration config; + config.m_twistLimitUpper = JointConstants::CreateJointDefaults::UpperLimit; + config.m_twistLimitLower = JointConstants::CreateJointDefaults::LowerLimit; + config.m_swingLimitY = 1.0f; + config.m_swingLimitZ = 1.0f; //build the head - joints.emplace_back(m_system->CreateJoint(config, snakeRigidBodies[0], snakeHead)); + jointHandles.emplace_back(m_defaultScene->AddJoint(&config, snakeRigidBodies[0]->m_bodyHandle, snakeHead->m_bodyHandle)); for (size_t i = 0; (i+1) < snakeRigidBodies.size(); i++) { - joints.emplace_back(m_system->CreateJoint(config, snakeRigidBodies[i + 1], snakeRigidBodies[i])); + jointHandles.emplace_back( + m_defaultScene->AddJoint(&config, snakeRigidBodies[i + 1]->m_bodyHandle, snakeRigidBodies[i]->m_bodyHandle)); } //setup the sub tick tracker @@ -404,6 +409,10 @@ namespace PhysX::Benchmarks subTickTracker.Stop(); m_defaultScene->RemoveSimulatedBodies(snakeRigidBodyHandles); + for (const auto& jointHandle : jointHandles) + { + m_defaultScene->RemoveJoint(jointHandle); + } snakeRigidBodyHandles.clear(); //sort the frame times and get the P50, P90, P99 percentiles diff --git a/Gems/PhysX/Code/Tests/PhysXGenericTestFixture.h b/Gems/PhysX/Code/Tests/PhysXGenericTestFixture.h index 80b338150f..1a1eefab46 100644 --- a/Gems/PhysX/Code/Tests/PhysXGenericTestFixture.h +++ b/Gems/PhysX/Code/Tests/PhysXGenericTestFixture.h @@ -111,7 +111,7 @@ namespace PhysX }; class GenericPhysicsInterfaceTest - : protected GenericPhysicsFixture + : public GenericPhysicsFixture , public testing::Test { public: diff --git a/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp b/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp index b976bbe77a..7d691d02a6 100644 --- a/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp +++ b/Gems/PhysX/Code/Tests/PhysXJointsTest.cpp @@ -18,17 +18,18 @@ #include #include -#include #include #include #include #include +#include #include #include #include #include #include +#include namespace PhysX { @@ -38,8 +39,9 @@ namespace PhysX AZStd::unique_ptr AddBodyColliderEntity( AzPhysics::SceneHandle sceneHandle, const AZ::Vector3& position, const AZ::Vector3& initialLinearVelocity, - AZStd::shared_ptr jointConfig = nullptr, - AZStd::shared_ptr jointLimitsConfig = nullptr) + AZStd::shared_ptr jointConfig = nullptr, + AZStd::shared_ptr jointGenericProperties = nullptr, + AZStd::shared_ptr jointLimitProperties = nullptr) { const char* entityName = "testEntity"; auto entity = AZStd::make_unique(entityName); @@ -68,10 +70,12 @@ namespace PhysX { jointConfig->m_followerEntity = entity->GetId(); - GenericJointLimitsConfiguration defaultJointLimitsConfig; + JointGenericProperties defaultJointGenericProperties; + JointLimitProperties defaultJointLimitProperties; entity->CreateComponent( *jointConfig, - (jointLimitsConfig)? *jointLimitsConfig : defaultJointLimitsConfig); + (jointGenericProperties)? *jointGenericProperties : defaultJointGenericProperties, + (jointLimitProperties)? *jointLimitProperties : defaultJointLimitProperties); } entity->Init(); @@ -114,7 +118,7 @@ namespace PhysX leadPosition, leadInitialLinearVelocity); - auto jointConfig = AZStd::make_shared(); + auto jointConfig = AZStd::make_shared(); jointConfig->m_leadEntity = leadEntity->GetId(); jointConfig->m_localTransformFromFollower = jointLocalTransform; @@ -150,17 +154,18 @@ namespace PhysX leadPosition, leadInitialLinearVelocity); - auto jointConfig = AZStd::make_shared(); + auto jointConfig = AZStd::make_shared(); jointConfig->m_leadEntity = leadEntity->GetId(); jointConfig->m_localTransformFromFollower = jointLocalTransform; - auto jointLimits = AZStd::make_shared (); + auto jointLimits = AZStd::make_shared (); jointLimits->m_isLimited = false; auto followerEntity = AddBodyColliderEntity(m_testSceneHandle, followerPosition, followerInitialLinearVelocity, jointConfig, + nullptr, jointLimits); const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); @@ -191,21 +196,104 @@ namespace PhysX leadPosition, leadInitialLinearVelocity); - auto jointConfig = AZStd::make_shared(); + auto jointConfig = AZStd::make_shared(); jointConfig->m_leadEntity = leadEntity->GetId(); jointConfig->m_localTransformFromFollower = jointLocalTransform; - auto jointLimits = AZStd::make_shared (); + auto jointLimits = AZStd::make_shared (); jointLimits->m_isLimited = false; auto followerEntity = AddBodyColliderEntity(m_testSceneHandle, followerPosition, followerInitialLinearVelocity, jointConfig, + nullptr, jointLimits); const AZ::Vector3 followerEndPosition = RunJointTest(m_defaultScene, followerEntity->GetId()); EXPECT_TRUE(followerEndPosition.GetZ() > followerPosition.GetZ()); } + +// for some reason TYPED_TEST_CASE with the fixture is not working on Android + Linux +#ifdef ENABLE_JOINTS_TYPED_TEST_CASE + template + class PhysXJointsApiTest : public PhysX::GenericPhysicsInterfaceTest + { + public: + + void SetUp() override + { + PhysX::GenericPhysicsInterfaceTest::SetUp(); + + if (auto* sceneInterface = AZ::Interface::Get()) + { + AzPhysics::RigidBodyConfiguration parentConfiguration; + AzPhysics::RigidBodyConfiguration childConfiguration; + + auto colliderConfig = AZStd::make_shared(); + auto shapeConfiguration = AZStd::make_shared(AZ::Vector3(1.0f, 1.0f, 1.0f)); + + parentConfiguration.m_colliderAndShapeData = AzPhysics::ShapeColliderPair(colliderConfig, shapeConfiguration); + childConfiguration.m_colliderAndShapeData = AzPhysics::ShapeColliderPair(colliderConfig, shapeConfiguration); + + // Put the child body a bit to the lower side of X to avoid it colliding with parent + childConfiguration.m_position.SetX(childConfiguration.m_position.GetX() - 2.0f); + m_childInitialPos = childConfiguration.m_position; + parentConfiguration.m_initialLinearVelocity.SetX(10.0f); + + m_parentBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &parentConfiguration); + m_childBodyHandle = sceneInterface->AddSimulatedBody(m_testSceneHandle, &childConfiguration); + } + } + + void TearDown() override + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->RemoveSimulatedBody(m_testSceneHandle, m_parentBodyHandle); + sceneInterface->RemoveSimulatedBody(m_testSceneHandle, m_childBodyHandle); + } + + PhysX::GenericPhysicsInterfaceTest::TearDown(); + } + + AzPhysics::SimulatedBodyHandle m_parentBodyHandle = AzPhysics::InvalidJointHandle; + AzPhysics::SimulatedBodyHandle m_childBodyHandle = AzPhysics::InvalidJointHandle; + AZ::Vector3 m_childInitialPos; + }; + + using JointTypes = testing::Types< + D6JointLimitConfiguration, + FixedJointConfiguration, + BallJointConfiguration, + HingeJointConfiguration>; + TYPED_TEST_CASE(PhysXJointsApiTest, JointTypes); + + TYPED_TEST(PhysXJointsApiTest, Joint_ChildFollowsParent) + { + TypeParam jointConfiguration; + AzPhysics::JointHandle jointHandle = AzPhysics::InvalidJointHandle; + + if (auto* sceneInterface = AZ::Interface::Get()) + { + jointHandle = sceneInterface->AddJoint(m_testSceneHandle, &jointConfiguration, m_parentBodyHandle, m_childBodyHandle); + } + + EXPECT_NE(jointHandle, AzPhysics::InvalidJointHandle); + + // run physics to trigger the the move of parent body + TestUtils::UpdateScene(m_testSceneHandle, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 1); + + AZ::Vector3 childCurrentPos; + + if (auto* sceneInterface = AZ::Interface::Get()) + { + auto* childBody = sceneInterface->GetSimulatedBodyFromHandle(m_testSceneHandle, m_childBodyHandle); + childCurrentPos = childBody->GetPosition(); + } + + EXPECT_GT(childCurrentPos.GetX(), m_childInitialPos.GetX()); + } +#endif // ENABLE_JOINTS_TYPED_TEST_CASE } diff --git a/Gems/PhysX/Code/Tests/RagdollConfiguration.xml b/Gems/PhysX/Code/Tests/RagdollConfiguration.xml index e1c2c5bed9..b3a268a14b 100644 --- a/Gems/PhysX/Code/Tests/RagdollConfiguration.xml +++ b/Gems/PhysX/Code/Tests/RagdollConfiguration.xml @@ -4,7 +4,7 @@ - + @@ -27,9 +27,9 @@ - - - + + + @@ -42,7 +42,7 @@ - + @@ -65,9 +65,9 @@ - - - + + + @@ -80,7 +80,7 @@ - + @@ -103,9 +103,9 @@ - - - + + + @@ -118,7 +118,7 @@ - + @@ -141,9 +141,9 @@ - - - + + + @@ -156,7 +156,7 @@ - + @@ -179,9 +179,9 @@ - - - + + + @@ -194,7 +194,7 @@ - + @@ -217,9 +217,9 @@ - - - + + + @@ -232,7 +232,7 @@ - + @@ -255,9 +255,9 @@ - - - + + + @@ -270,7 +270,7 @@ - + @@ -293,9 +293,9 @@ - - - + + + @@ -308,7 +308,7 @@ - + @@ -331,9 +331,9 @@ - - - + + + @@ -346,7 +346,7 @@ - + @@ -369,9 +369,9 @@ - - - + + + @@ -384,7 +384,7 @@ - + @@ -407,9 +407,9 @@ - - - + + + @@ -422,7 +422,7 @@ - + @@ -445,9 +445,9 @@ - - - + + + @@ -460,7 +460,7 @@ - + @@ -483,9 +483,9 @@ - - - + + + @@ -498,7 +498,7 @@ - + @@ -521,9 +521,9 @@ - - - + + + @@ -536,7 +536,7 @@ - + @@ -559,9 +559,9 @@ - - - + + + @@ -574,7 +574,7 @@ - + @@ -597,9 +597,9 @@ - - - + + + @@ -612,7 +612,7 @@ - + @@ -635,9 +635,9 @@ - - - + + + @@ -650,7 +650,7 @@ - + @@ -673,9 +673,9 @@ - - - + + + @@ -688,7 +688,7 @@ - + @@ -711,9 +711,9 @@ - - - + + + @@ -726,7 +726,7 @@ - + @@ -749,9 +749,9 @@ - - - + + + @@ -764,7 +764,7 @@ - + @@ -787,9 +787,9 @@ - - - + + + @@ -802,7 +802,7 @@ - + @@ -825,9 +825,9 @@ - - - + + + diff --git a/Gems/PhysX/Code/Tests/RagdollTests.cpp b/Gems/PhysX/Code/Tests/RagdollTests.cpp index 477e754d74..816e7e2884 100644 --- a/Gems/PhysX/Code/Tests/RagdollTests.cpp +++ b/Gems/PhysX/Code/Tests/RagdollTests.cpp @@ -208,8 +208,8 @@ namespace PhysX } else { - EXPECT_EQ(joint->GetChildBody(), &node->GetRigidBody()); - EXPECT_EQ(joint->GetParentBody(), &ragdoll->GetNode(parentIndex)->GetRigidBody()); + EXPECT_EQ(joint->GetChildBodyHandle(), node->GetRigidBody().m_bodyHandle); + EXPECT_EQ(joint->GetParentBodyHandle(), ragdoll->GetNode(parentIndex)->GetRigidBody().m_bodyHandle); } } } diff --git a/Gems/PhysX/Code/physx_files.cmake b/Gems/PhysX/Code/physx_files.cmake index 24aa42d62a..3f5589242a 100644 --- a/Gems/PhysX/Code/physx_files.cmake +++ b/Gems/PhysX/Code/physx_files.cmake @@ -75,8 +75,6 @@ set(FILES Source/Shape.cpp Source/Material.cpp Source/Material.h - Source/Joint.cpp - Source/Joint.h Source/ForceRegionForces.cpp Source/ForceRegionForces.h Source/ForceRegion.cpp @@ -104,6 +102,7 @@ set(FILES Include/PhysX/Debug/PhysXDebugConfiguration.h Include/PhysX/Debug/PhysXDebugInterface.h Include/PhysX/Configuration/PhysXConfiguration.h + Include/PhysX/Joint/Configuration/PhysXJointConfiguration.h Source/Common/PhysXSceneQueryHelpers.h Source/Common/PhysXSceneQueryHelpers.cpp Source/Configuration/PhysXConfiguration.cpp @@ -112,6 +111,11 @@ set(FILES Source/Debug/PhysXDebug.h Source/Debug/PhysXDebug.cpp Source/Debug/Configuration/PhysXDebugConfiguration.cpp + Source/Joint/PhysXJoint.h + Source/Joint/PhysXJoint.cpp + Source/Joint/PhysXJointUtils.h + Source/Joint/PhysXJointUtils.cpp + Source/Joint/Configuration/PhysXJointConfiguration.cpp Source/Scene/PhysXScene.h Source/Scene/PhysXScene.cpp Source/Scene/PhysXSceneInterface.h @@ -128,6 +132,8 @@ set(FILES Source/System/PhysXCpuDispatcher.h Source/System/PhysXJob.cpp Source/System/PhysXJob.h + Source/System/PhysXJointInterface.h + Source/System/PhysXJointInterface.cpp Source/System/PhysXSdkCallbacks.h Source/System/PhysXSdkCallbacks.cpp Source/System/PhysXSystem.h diff --git a/Gems/PhysXDebug/Code/Source/SystemComponent.cpp b/Gems/PhysXDebug/Code/Source/SystemComponent.cpp index c693599a59..3a5499368b 100644 --- a/Gems/PhysXDebug/Code/Source/SystemComponent.cpp +++ b/Gems/PhysXDebug/Code/Source/SystemComponent.cpp @@ -783,7 +783,7 @@ namespace PhysXDebug Physics::RagdollNode* ragdollNode = actorData->GetRagdollNode(); if (ragdollNode) { - const AZStd::shared_ptr& joint = ragdollNode->GetJoint(); + AzPhysics::Joint* joint = ragdollNode->GetJoint(); physx::PxJoint* pxJoint = static_cast(joint->GetNativePointer()); physx::PxTransform jointPose = actor1->getGlobalPose() * pxJoint->getLocalPose(physx::PxJointActorIndex::eACTOR1); if (!m_culling.m_enabled || m_cullingBox.contains(jointPose.p)) diff --git a/Gems/PhysXSamples/Assets/ScriptCanvas/Weapons/Revolver/Tracer_FX.scriptcanvas b/Gems/PhysXSamples/Assets/ScriptCanvas/Weapons/Revolver/Tracer_FX.scriptcanvas index eebc79585d..f0fff5e69f 100644 --- a/Gems/PhysXSamples/Assets/ScriptCanvas/Weapons/Revolver/Tracer_FX.scriptcanvas +++ b/Gems/PhysXSamples/Assets/ScriptCanvas/Weapons/Revolver/Tracer_FX.scriptcanvas @@ -3,12 +3,12 @@ - + - + - - + + @@ -16,19 +16,21 @@ - + - + - + - + + + - + @@ -60,10 +62,13 @@ + - + + + - + @@ -95,10 +100,13 @@ + - + + + - + @@ -130,10 +138,13 @@ + - + + + - + @@ -165,10 +176,13 @@ + - + + + - + @@ -200,10 +214,13 @@ + - + + + - + @@ -235,22 +252,22 @@ + - - + - + - + - + @@ -260,7 +277,7 @@ - + @@ -270,7 +287,7 @@ - + @@ -281,104 +298,26 @@ - + - + - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -389,7 +328,7 @@ - + @@ -410,10 +349,13 @@ + - + + + - + @@ -424,7 +366,7 @@ - + @@ -445,69 +387,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -516,44 +402,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + @@ -562,22 +418,25 @@ - - + + - + + - + + + - + @@ -609,10 +468,13 @@ + - + + + - + @@ -644,10 +506,13 @@ + - + + + - + @@ -679,10 +544,13 @@ + - + + + - + @@ -714,22 +582,38 @@ + - - - + + + + + + + + + + + + + + + + + + - + - + @@ -739,7 +623,7 @@ - + @@ -749,7 +633,7 @@ - + @@ -760,24 +644,26 @@ - + - + - + - - + + - + - + + + - + @@ -788,7 +674,7 @@ - + @@ -809,10 +695,13 @@ + - + + + - + @@ -822,8 +711,8 @@ - - + + @@ -844,10 +733,13 @@ + - + + + - + @@ -856,9 +748,14 @@ + + + + + - - + + @@ -867,22 +764,25 @@ - - + + - + + - + + + - + @@ -891,23 +791,18 @@ - - - - - - - + + - + - + @@ -919,6 +814,7 @@ + @@ -932,32 +828,43 @@ - + - - + + + + + + + + + + + - + - + - + - - + + - + - + + + - + @@ -968,7 +875,7 @@ - + @@ -989,10 +896,13 @@ + - + + + - + @@ -1003,7 +913,7 @@ - + @@ -1024,10 +934,13 @@ + - + + + - + @@ -1036,23 +949,18 @@ - - - - - - + - + - + @@ -1064,10 +972,13 @@ + - + + + - + @@ -1076,23 +987,18 @@ - - - - - - + - + - + @@ -1104,10 +1010,13 @@ + - + + + - + @@ -1117,10 +1026,10 @@ - + - + @@ -1139,60 +1048,110 @@ + - - - - - - - + + + + + - - - - + + + + + + - - - - - + + + - - - + + - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - - + + - + - + + + - + @@ -1203,7 +1162,7 @@ - + @@ -1224,10 +1183,13 @@ + - + + + - + @@ -1238,7 +1200,7 @@ - + @@ -1259,10 +1221,13 @@ + - + + + - + @@ -1271,23 +1236,18 @@ - - - - - - + - + - + @@ -1299,10 +1259,13 @@ + - + + + - + @@ -1311,23 +1274,18 @@ - - - - - - + - + - + @@ -1339,10 +1297,13 @@ + - + + + - + @@ -1352,10 +1313,10 @@ - + - + @@ -1374,58 +1335,110 @@ + - - - - - - - + + + + + - - - + + + + + + + - - - - - + + + - - - + + - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - - + + - + - + + + - + @@ -1434,9 +1447,14 @@ + + + + + - - + + @@ -1446,21 +1464,24 @@ - + - + + - + + + - + @@ -1469,9 +1490,14 @@ + + + + + - - + + @@ -1480,22 +1506,25 @@ - - + + - + + - + + + - + @@ -1504,18 +1533,23 @@ + + + + + - + - + - + @@ -1527,39 +1561,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -1570,7 +1578,7 @@ - + @@ -1591,10 +1599,13 @@ + - + + + - + @@ -1605,7 +1616,7 @@ - + @@ -1626,10 +1637,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1639,10 +1723,10 @@ - + - + @@ -1661,10 +1745,13 @@ + - + + + - + @@ -1673,18 +1760,23 @@ + + + + + - + - + - + @@ -1696,10 +1788,13 @@ + - + + + - + @@ -1708,18 +1803,23 @@ + + + + + - + - + - + @@ -1731,220 +1831,13 @@ + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1981,8 +1874,11 @@ + - + + + @@ -2016,8 +1912,11 @@ + - + + + @@ -2051,6 +1950,7 @@ + @@ -2091,8 +1991,7 @@ - - + @@ -2106,24 +2005,26 @@ - + - + - + - - + + - + - + + + - + @@ -2133,8 +2034,8 @@ - - + + @@ -2155,10 +2056,13 @@ + - + + + - + @@ -2168,8 +2072,8 @@ - - + + @@ -2178,7 +2082,7 @@ - + @@ -2190,10 +2094,13 @@ + - + + + - + @@ -2203,10 +2110,10 @@ - - + + - + @@ -2214,21 +2121,24 @@ - + - + + - + + + - + @@ -2238,10 +2148,10 @@ - - + + - + @@ -2249,21 +2159,24 @@ - + - + + - + + + - + @@ -2273,10 +2186,10 @@ - - + + - + @@ -2284,21 +2197,24 @@ - + - + + - + + + - + @@ -2307,18 +2223,35 @@ + + + + + + + + + + + + + + + + + - - + + - + - + @@ -2330,70 +2263,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -2403,10 +2279,10 @@ - + - + @@ -2425,10 +2301,13 @@ + - + + + - + @@ -2437,13 +2316,8 @@ - - - - - - + @@ -2453,22 +2327,25 @@ - - + + - + - + + - + + + - + @@ -2478,32 +2355,35 @@ - + - + - - + + - + + - + + + - + @@ -2513,7 +2393,7 @@ - + @@ -2526,7 +2406,7 @@ - + @@ -2535,6 +2415,7 @@ + @@ -2550,42 +2431,88 @@ - + - - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + - + - + - - + + - + - + + + - + @@ -2596,7 +2523,7 @@ - + @@ -2617,10 +2544,13 @@ + - + + + - + @@ -2631,7 +2561,7 @@ - + @@ -2652,10 +2582,13 @@ + - + + + - + @@ -2664,23 +2597,18 @@ - - - - - - + - + - + @@ -2692,10 +2620,13 @@ + - + + + - + @@ -2705,10 +2636,10 @@ - + - + @@ -2727,197 +2658,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -2949,10 +2696,13 @@ + - + + + - + @@ -2984,22 +2734,22 @@ + - - + - + - + - + @@ -3009,7 +2759,7 @@ - + @@ -3019,7 +2769,7 @@ - + @@ -3030,24 +2780,26 @@ - + - + - + - - + + - + - + + + - + @@ -3058,7 +2810,7 @@ - + @@ -3079,10 +2831,13 @@ + - + + + - + @@ -3093,7 +2848,7 @@ - + @@ -3114,10 +2869,56 @@ + - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3149,10 +2950,13 @@ + - + + + - + @@ -3184,10 +2988,13 @@ + - + + + - + @@ -3219,10 +3026,13 @@ + - + + + - + @@ -3254,22 +3064,38 @@ + - - - + + + + + + + + + + + + + + + - + + + + - + - + @@ -3279,7 +3105,7 @@ - + @@ -3289,7 +3115,7 @@ - + @@ -3300,584 +3126,246 @@ - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + - - - - - - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - + - + - + - + - + - + + + - + @@ -3909,10 +3397,13 @@ + - + + + - + @@ -3944,10 +3435,13 @@ + - + + + - + @@ -3957,10 +3451,10 @@ - + - + @@ -3979,109 +3473,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -4091,10 +3489,10 @@ - + - + @@ -4113,10 +3511,13 @@ + - + + + - + @@ -4126,7 +3527,7 @@ - + @@ -4148,10 +3549,13 @@ + - + + + - + @@ -4161,42 +3565,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -4218,22 +3587,22 @@ + - - + - + - + @@ -4243,7 +3612,7 @@ - + @@ -4253,7 +3622,7 @@ - + @@ -4264,24 +3633,26 @@ - + - + - + - - + + - + - + + + - + @@ -4290,14 +3661,9 @@ - - - - - - - + + @@ -4307,21 +3673,24 @@ - + - + + - + + + - + @@ -4330,23 +3699,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + @@ -4358,10 +3760,13 @@ + - + + + - + @@ -4371,32 +3776,35 @@ - + - + - - + + - + + - + + + - + @@ -4406,10 +3814,10 @@ - + - + @@ -4417,80 +3825,121 @@ - + - + + - - - - - - - + + + + + - - - - + + + + + + - - - - - - + + + + - - - + + + + + + + + + + + + + + + - + - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - - + + - + - + + + - + @@ -4500,32 +3949,35 @@ - - + + - + - - + + - + + - + + + - + @@ -4534,9 +3986,14 @@ + + + + + - - + + @@ -4546,21 +4003,24 @@ - + - + + - + + + - + @@ -4570,8 +4030,8 @@ - - + + @@ -4580,7 +4040,7 @@ - + @@ -4592,10 +4052,13 @@ + - + + + - + @@ -4605,8 +4068,8 @@ - - + + @@ -4627,45 +4090,59 @@ + - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -4674,26 +4151,9 @@ - - - - - - - - - - - - - - - - - - - + + @@ -4703,21 +4163,24 @@ - + - + + - + + + - + @@ -4727,10 +4190,10 @@ - + - + @@ -4738,21 +4201,24 @@ - + - + + - + + + - + @@ -4761,8 +4227,13 @@ + + + + + - + @@ -4772,22 +4243,25 @@ - - + + - + + - + + + - + @@ -4796,18 +4270,23 @@ + + + + + - + - + - + @@ -4819,10 +4298,13 @@ + - + + + - + @@ -4832,10 +4314,10 @@ - + - + @@ -4843,17 +4325,18 @@ - + - + + @@ -4869,85 +4352,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + + + + + - - - - + + + - - + + - - - - - + - + - + - + - + + + @@ -4981,8 +4423,11 @@ + - + + + @@ -5016,8 +4461,11 @@ + - + + + @@ -5051,70 +4499,757 @@ + - - + - + - + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5143,10 +5278,13 @@ + - + + + - + @@ -5178,10 +5316,13 @@ + - + + + - + @@ -5213,10 +5354,13 @@ + - + + + - + @@ -5248,10 +5392,13 @@ + - + + + - + @@ -5283,22 +5430,22 @@ + - - + - + - + - + @@ -5308,7 +5455,7 @@ - + @@ -5318,7 +5465,7 @@ - + @@ -5329,24 +5476,26 @@ - + - + - + - + - + + + - + @@ -5378,10 +5527,13 @@ + - + + + - + @@ -5413,10 +5565,13 @@ + - + + + - + @@ -5448,10 +5603,13 @@ + - + + + - + @@ -5483,10 +5641,13 @@ + - + + + - + @@ -5518,10 +5679,13 @@ + - + + + - + @@ -5553,22 +5717,22 @@ + - - + - + - + - + @@ -5578,7 +5742,7 @@ - + @@ -5588,7 +5752,7 @@ - + @@ -5599,24 +5763,26 @@ - + - + - + - - + + - + - + + + - + @@ -5627,7 +5793,7 @@ - + @@ -5648,10 +5814,13 @@ + - + + + - + @@ -5662,7 +5831,7 @@ - + @@ -5683,10 +5852,13 @@ + - + + + - + @@ -5695,18 +5867,23 @@ + + + + + - + - + - + @@ -5718,10 +5895,13 @@ + - + + + - + @@ -5731,10 +5911,10 @@ - + - + @@ -5753,23 +5933,72 @@ + - - - + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + @@ -5788,10 +6017,13 @@ + - + + + - + @@ -5800,18 +6032,23 @@ + + + + + - + - + - + @@ -5823,72 +6060,137 @@ + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + - + - + - - + + - + - + + + - + - + @@ -5900,35 +6202,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + @@ -5940,19 +6218,22 @@ - + + - + + + - + - + @@ -5964,35 +6245,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + @@ -6004,19 +6261,22 @@ - + + - + + + - + - + @@ -6028,35 +6288,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + @@ -6068,17 +6304,20 @@ - + + - + + + - + @@ -6093,8 +6332,8 @@ - - + + @@ -6115,104 +6354,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -6244,45 +6392,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -6314,81 +6430,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -6397,31 +6444,31 @@ - + - + - + - + - + - + - + - + - + @@ -6431,36 +6478,43 @@ - + - + - - + - - - + + + + + + + + + - + - + - + - + + + @@ -6494,8 +6548,11 @@ + - + + + @@ -6529,8 +6586,11 @@ + - + + + @@ -6564,8 +6624,11 @@ + - + + + @@ -6599,8 +6662,11 @@ + - + + + @@ -6634,8 +6700,11 @@ + - + + + @@ -6669,11 +6738,11 @@ + - - + @@ -6715,24 +6784,26 @@ - + - + - + - + - + + + - + @@ -6764,10 +6835,13 @@ + - + + + - + @@ -6799,10 +6873,13 @@ + - + + + - + @@ -6834,10 +6911,13 @@ + - + + + - + @@ -6869,10 +6949,13 @@ + - + + + - + @@ -6904,10 +6987,13 @@ + - + + + - + @@ -6939,22 +7025,22 @@ + - - + - + - + - + @@ -6964,7 +7050,7 @@ - + @@ -6974,7 +7060,7 @@ - + @@ -6985,24 +7071,26 @@ - + - + - + - - + + - + - + + + - + @@ -7011,13 +7099,8 @@ - - - - - - + @@ -7028,21 +7111,24 @@ - + - + + - + + + - + @@ -7051,11 +7137,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - @@ -7064,7 +7181,7 @@ - + @@ -7086,10 +7203,13 @@ + - + + + - + @@ -7098,8 +7218,13 @@ + + + + + - + @@ -7110,21 +7235,24 @@ - + - + + - + + + - + @@ -7134,10 +7262,10 @@ - + - + @@ -7145,17 +7273,18 @@ - + - + + @@ -7171,54 +7300,46 @@ - + - + - + - + - - + - - - - - - - - - - + - + - + - + - - + + - + - + + + - + @@ -7227,9 +7348,14 @@ + + + + + - - + + @@ -7239,21 +7365,24 @@ - + - + + - + + + - + @@ -7262,9 +7391,21 @@ + + + + + + + + + + + + - - + + @@ -7273,22 +7414,25 @@ - - + + - + + - + + + - + @@ -7297,13 +7441,8 @@ - - - - - - + @@ -7314,21 +7453,24 @@ - + - + + - + + + - + @@ -7338,10 +7480,10 @@ - + - + @@ -7349,21 +7491,82 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -7373,32 +7576,35 @@ - - + + - + - - + + - + + - + + + - + @@ -7408,10 +7614,10 @@ - - + + - + @@ -7419,21 +7625,24 @@ - + - + + - + + + - + @@ -7443,7 +7652,7 @@ - + @@ -7465,86 +7674,41 @@ + - - - - - - - - - - - - - - - - + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - + - - + + - + - + + + - + @@ -7555,7 +7719,7 @@ - + @@ -7576,10 +7740,13 @@ + - + + + - + @@ -7590,7 +7757,7 @@ - + @@ -7611,50 +7778,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -7686,10 +7816,13 @@ + - + + + - + @@ -7721,10 +7854,13 @@ + - + + + - + @@ -7756,10 +7892,13 @@ + - + + + - + @@ -7791,38 +7930,22 @@ + - - - - - - - - - - - - - - - - + + - - - - + - + - + @@ -7832,7 +7955,7 @@ - + @@ -7842,7 +7965,7 @@ - + @@ -7853,24 +7976,26 @@ - + - + - + - - + + - + - + + + - + @@ -7879,14 +8004,9 @@ - - - - - - - + + @@ -7896,21 +8016,24 @@ - + - + + - + + + - + @@ -7919,14 +8042,9 @@ - - - - - - - + + @@ -7935,22 +8053,25 @@ - - + + - + + - + + + - + @@ -7959,23 +8080,18 @@ - - - - - - + - + - + @@ -7987,10 +8103,13 @@ + - + + + - + @@ -8000,32 +8119,35 @@ - + - + - - + + - + + - + + + - + @@ -8035,10 +8157,10 @@ - + - + @@ -8046,132 +8168,121 @@ - + - + + - - - - - - - + + + + + - - - - + + + + + + - - - - - - + + + + - - - + + - - - - - - - + + + - - - + + + - + + + + + + - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -8180,14 +8291,9 @@ - - - - - - - + + @@ -8197,21 +8303,24 @@ - + - + + - + + + - + @@ -8221,8 +8330,8 @@ - - + + @@ -8231,7 +8340,7 @@ - + @@ -8243,10 +8352,13 @@ + - + + + - + @@ -8256,10 +8368,10 @@ - + - + @@ -8267,80 +8379,52 @@ - + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + + - + + + + - + - + - + - + - + - + + + - + @@ -8349,18 +8433,23 @@ + + + + + - + - + - + @@ -8372,10 +8461,13 @@ + - + + + - + @@ -8384,13 +8476,20 @@ + + + + + + + - + @@ -8412,10 +8511,13 @@ + - + + + - + @@ -8447,10 +8549,13 @@ + - + + + - + @@ -8482,6 +8587,7 @@ + @@ -8499,500 +8605,476 @@ + + + + + + + + + + + + - - + - + - + - + - + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - + - + - + - - + + - + - + + + - + @@ -9001,14 +9083,9 @@ - - - - - - - + + @@ -9018,21 +9095,24 @@ - + - + + - + + + - + @@ -9041,14 +9121,9 @@ - - - - - - - + + @@ -9057,22 +9132,25 @@ - - + + - + + - + + + - + @@ -9082,8 +9160,8 @@ - - + + @@ -9092,7 +9170,7 @@ - + @@ -9104,10 +9182,13 @@ + - + + + - + @@ -9116,9 +9197,14 @@ + + + + + - - + + @@ -9127,81 +9213,59 @@ - - + + - + + - - - - - - - - - - - - - - - + - + - + - - - - - - - - - - - + - - + - + - + - - + + - + - + + + - + @@ -9211,10 +9275,48 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -9222,21 +9324,24 @@ - + - + + - + + + - + @@ -9251,7 +9356,7 @@ - + @@ -9273,10 +9378,13 @@ + - + + + - + @@ -9285,8 +9393,13 @@ + + + + + - + @@ -9297,21 +9410,24 @@ - + - + + - + + + - + @@ -9321,10 +9437,10 @@ - + - + @@ -9332,68 +9448,71 @@ - + - + + - + - - - + - + + + + + + + + + + + + + - - - - - - - - - - - + - + - + - + - + - - + + - + - + + + - + @@ -9403,32 +9522,35 @@ - - + + - + - - + + - + + - + + + - + @@ -9437,14 +9559,9 @@ - - - - - - - + + @@ -9453,22 +9570,25 @@ - - + + - + + - + + + - + @@ -9478,32 +9598,35 @@ - + - + - - + + - + + - + + + - + @@ -9513,10 +9636,10 @@ - + - + @@ -9524,864 +9647,814 @@ - + - + + - - - - - - - + + + + + - - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - + + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - + + + + + - - - - - + + + + - - - - - - - - - - + - + - + - + + + @@ -10415,8 +10488,11 @@ + - + + + @@ -10450,8 +10526,11 @@ + - + + + @@ -10490,8 +10569,11 @@ + - + + + @@ -10525,8 +10607,11 @@ + - + + + @@ -10560,8 +10645,11 @@ + - + + + @@ -10595,8 +10683,11 @@ + - + + + @@ -10630,6 +10721,7 @@ + @@ -10646,8 +10738,7 @@ - - + @@ -10692,24 +10783,26 @@ - + - + - + - - + + - + - + + + - + @@ -10718,9 +10811,14 @@ + + + + + - - + + @@ -10730,21 +10828,24 @@ - + - + + - + + + - + @@ -10753,53 +10854,23 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + @@ -10811,39 +10882,13 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - + @@ -10854,7 +10899,7 @@ - + @@ -10875,10 +10920,13 @@ + - + + + - + @@ -10889,7 +10937,7 @@ - + @@ -10910,244 +10958,729 @@ + - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + + + - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -11157,2578 +11690,847 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + - - - + + + + + - - - - + + + + + - - - - - - - + + + + - - - + + + + + - - - - - - + + + + - - - + + + + + - - - - - - + + + + - - - + + + + + - - - - - - + + + + - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -13736,30 +12538,202 @@ - + - + - - - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + - - + + + + + + + + + + @@ -13767,61 +12741,141 @@ - + - + - - - - - - - - - - - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + + + @@ -13829,90 +12883,172 @@ - + - + - - - - - - - - - - - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - + + + + + - + - + - + - + - + - + - + @@ -13922,28 +13058,28 @@ - + - + - + - + - + - + - + @@ -13953,28 +13089,28 @@ - + - + - + - + - + - + - + @@ -13984,28 +13120,28 @@ - + - + - + - + - + - + - + @@ -14015,28 +13151,28 @@ - + - + - + - + - + - + - + @@ -14046,28 +13182,28 @@ - + - + - + - + - + - + - + @@ -14077,28 +13213,28 @@ - + - + - + - + - + - + - + @@ -14108,28 +13244,28 @@ - + - + - + - + - + - + - + @@ -14139,28 +13275,28 @@ - + - + - + - + - + - + - + @@ -14170,28 +13306,28 @@ - + - + - + - + - + - + - + @@ -14201,28 +13337,28 @@ - + - + - + - + - + - + - + @@ -14232,28 +13368,28 @@ - + - + - + - + - + - + - + @@ -14263,28 +13399,28 @@ - + - + - + - + - + - + - + @@ -14294,28 +13430,28 @@ - + - + - + - + - + - + - + @@ -14325,28 +13461,28 @@ - + - + - + - + - + - + - + @@ -14356,28 +13492,28 @@ - + - + - + - + - + - + - + @@ -14387,28 +13523,28 @@ - + - + - + - + - + - + - + @@ -14418,28 +13554,28 @@ - + - + - + - + - + - + - + @@ -14449,28 +13585,28 @@ - + - + - + - + - + - + - + @@ -14480,28 +13616,28 @@ - + - + - + - + - + - + - + @@ -14511,28 +13647,28 @@ - + - + - + - + - + - + - + @@ -14542,28 +13678,28 @@ - + - + - + - + - + - + - + @@ -14573,28 +13709,28 @@ - + - + - + - + - + - + - + @@ -14604,28 +13740,28 @@ - + - + - + - + - + - + - + @@ -14635,28 +13771,28 @@ - + - + - + - + - + - + - + @@ -14666,28 +13802,28 @@ - + - + - + - + - + - + - + @@ -14697,28 +13833,28 @@ - + - + - + - + - + - + - + @@ -14728,28 +13864,28 @@ - + - + - + - + - + - + - + @@ -14759,28 +13895,28 @@ - + - + - + - + - + - + - + @@ -14790,28 +13926,28 @@ - + - + - + - + - + - + - + @@ -14821,28 +13957,28 @@ - + - + - + - + - + - + - + @@ -14852,28 +13988,28 @@ - + - + - + - + - + - + - + @@ -14883,28 +14019,28 @@ - + - + - + - + - + - + - + @@ -14914,28 +14050,28 @@ - + - + - + - + - + - + - + @@ -14945,28 +14081,28 @@ - + - + - + - + - + - + - + @@ -14976,28 +14112,28 @@ - + - + - + - + - + - + - + @@ -15007,28 +14143,28 @@ - + - + - + - + - + - + - + @@ -15038,28 +14174,28 @@ - + - + - + - + - + - + - + @@ -15069,17 +14205,17 @@ - + - + - + @@ -15087,7 +14223,7 @@ - + @@ -15100,17 +14236,17 @@ - + - + - + @@ -15118,7 +14254,7 @@ - + @@ -15131,17 +14267,17 @@ - + - + - + @@ -15149,7 +14285,7 @@ - + @@ -15162,17 +14298,17 @@ - + - + - + @@ -15180,7 +14316,7 @@ - + @@ -15193,17 +14329,17 @@ - + - + - + @@ -15211,7 +14347,7 @@ - + @@ -15224,17 +14360,17 @@ - + - + - + @@ -15242,7 +14378,7 @@ - + @@ -15255,17 +14391,17 @@ - + - + - + @@ -15273,7 +14409,7 @@ - + @@ -15286,17 +14422,17 @@ - + - + - + @@ -15304,7 +14440,7 @@ - + @@ -15317,17 +14453,17 @@ - + - + - + @@ -15335,7 +14471,7 @@ - + @@ -15348,17 +14484,17 @@ - + - + - + @@ -15366,7 +14502,7 @@ - + @@ -15379,17 +14515,17 @@ - + - + - + @@ -15397,7 +14533,7 @@ - + @@ -15410,17 +14546,17 @@ - + - + - + @@ -15428,7 +14564,7 @@ - + @@ -15441,17 +14577,17 @@ - + - + - + @@ -15459,7 +14595,7 @@ - + @@ -15472,17 +14608,17 @@ - + - + - + @@ -15490,7 +14626,7 @@ - + @@ -15503,17 +14639,17 @@ - + - + - + @@ -15521,7 +14657,7 @@ - + @@ -15534,28 +14670,121 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + @@ -15565,28 +14794,28 @@ - + - + - + - + - + - + - + @@ -15596,28 +14825,28 @@ - + - + - + - + - + - + - + @@ -15627,28 +14856,28 @@ - + - + - + - + - + - + - + @@ -15658,28 +14887,28 @@ - + - + - + - + - + - + - + @@ -15689,28 +14918,28 @@ - + - + - + - + - + - + - + @@ -15720,28 +14949,28 @@ - + - + - + - + - + - + - + @@ -15751,28 +14980,28 @@ - + - + - + - + - + - + - + @@ -15782,28 +15011,28 @@ - + - + - + - + - + - + - + @@ -15813,404 +15042,310 @@ - + - + - + - + - + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16218,41 +15353,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16260,7 +15395,7 @@ - + @@ -16268,27 +15403,27 @@ - + - + - + - + @@ -16296,35 +15431,49 @@ - + - - + + - + + + + + + + + + - - + + - + - + - + + + + + + + @@ -16332,20 +15481,22 @@ - + - - - + + + + - - - + + + + @@ -16356,17 +15507,9 @@ - - - - - - - - - - - + + + @@ -16374,41 +15517,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16416,7 +15559,7 @@ - + @@ -16424,27 +15567,99 @@ - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -16452,7 +15667,7 @@ - + @@ -16460,7 +15675,7 @@ - + @@ -16480,7 +15695,7 @@ - + @@ -16488,41 +15703,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16530,7 +15745,7 @@ - + @@ -16538,7 +15753,7 @@ - + @@ -16558,7 +15773,13 @@ - + + + + + + + @@ -16566,41 +15787,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16608,41 +15829,35 @@ - + - - - + + + + - - - + + + + - - - - - - - - + - - - - + + + @@ -16650,7 +15865,7 @@ - + @@ -16658,27 +15873,27 @@ - + - + - + - + @@ -16686,41 +15901,35 @@ - + - - - + + + + - - - + + + + - - - - - - - - + - - - - + + + @@ -16728,7 +15937,7 @@ - + @@ -16736,7 +15945,7 @@ - + @@ -16756,7 +15965,13 @@ - + + + + + + + @@ -16764,49 +15979,35 @@ - + - - - + + + + - - - + + + + - - - - - - - - + - - - - - - - - - - - - + + + @@ -16814,41 +16015,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16856,41 +16057,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -16898,7 +16099,7 @@ - + @@ -16906,27 +16107,27 @@ - + - + - + - + @@ -16934,7 +16135,7 @@ - + @@ -16942,7 +16143,7 @@ - + @@ -16962,7 +16163,7 @@ - + @@ -16970,7 +16171,7 @@ - + @@ -16998,7 +16199,13 @@ - + + + + + + + @@ -17006,7 +16213,7 @@ - + @@ -17042,7 +16249,7 @@ - + @@ -17050,27 +16257,27 @@ - + - + - + - + @@ -17078,41 +16285,35 @@ - + - - - - - - - - - - - - - - + + - + - + - - + + - + + + + + + + @@ -17120,7 +16321,7 @@ - + @@ -17128,7 +16329,7 @@ - + @@ -17148,72 +16349,13 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -17221,41 +16363,35 @@ - + - - - + + + + - - - + + + + - - - - - - - - + - - - - + + + @@ -17263,7 +16399,7 @@ - + @@ -17271,7 +16407,7 @@ - + @@ -17291,7 +16427,7 @@ - + @@ -17299,7 +16435,7 @@ - + @@ -17307,27 +16443,33 @@ - + - + - + - + + + + + + + @@ -17335,20 +16477,22 @@ - + - - - + + + + - - - + + + + @@ -17359,17 +16503,15 @@ - - - - + + + - - - - + + + @@ -17377,41 +16519,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -17419,41 +16561,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -17461,41 +16603,71 @@ - + - - - + + + + - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + + + + + + + @@ -17503,7 +16675,7 @@ - + @@ -17511,7 +16683,7 @@ - + @@ -17531,7 +16703,7 @@ - + @@ -17539,7 +16711,7 @@ - + @@ -17547,27 +16719,33 @@ - + - + - + - + + + + + + + @@ -17575,41 +16753,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -17617,7 +16795,7 @@ - + @@ -17653,35 +16831,46 @@ - + - - + + - + + + + + + - - + + + + + + + + - + - + - - - + + + @@ -17689,41 +16878,41 @@ - + - - - + + + + - - - + + + + - + - - - - + + + - - - - + + + @@ -17731,7 +16920,7 @@ - + @@ -17739,27 +16928,33 @@ - + - + - + - + + + + + + + @@ -17767,20 +16962,22 @@ - + - - - + + + + - - - + + + + @@ -17790,18 +16987,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + + + + + + + @@ -17809,7 +17040,7 @@ - + @@ -17817,27 +17048,27 @@ - + - + - + - + @@ -17845,7 +17076,7 @@ - + @@ -17853,27 +17084,27 @@ - + - + - + - + @@ -17894,19 +17125,15 @@ - - + + - + - - - - - + @@ -17914,36 +17141,28 @@ - - - - - - - - - + - - + + - + - - + + - + - - + + @@ -17954,79 +17173,79 @@ - - + + - + - - + + - + - - + + - + - - + + - + - + - + - + - + - - + + - + - - + + - - + + - - + + - + @@ -18042,20 +17261,9 @@ - + - - - - - - - - - - - - + @@ -18064,62 +17272,64 @@ - + - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + - + - - - + - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + @@ -18130,27 +17340,62 @@ - + - + + + + + + + + + + + + + + - + - - - - + + + + + + + + + + + + + + + + - + + + + + + + + + + + @@ -18163,25 +17408,27 @@ + + + + + + + + + + + + + - - - - - - - - - - - - + @@ -18194,25 +17441,27 @@ - - - - - - - - - - - - + + + + - + + + + + + + + + + + @@ -18223,58 +17472,29 @@ - - - - - - - - - - - - - + - - + + + - - - - - - - - - - - + + - + + + - + - - - - - - - - - - - - + @@ -18283,42 +17503,59 @@ - + - - - - - - - - - - - - - + + + + + - + + + + + + + + + + + - + - + + + - + - + + + + + + + + + + + + + + diff --git a/Gems/ScriptCanvas/Code/Editor/Model/UnitTestBrowserFilterModel.cpp b/Gems/ScriptCanvas/Code/Editor/Model/UnitTestBrowserFilterModel.cpp index 665c81fa57..1a4d95c585 100644 --- a/Gems/ScriptCanvas/Code/Editor/Model/UnitTestBrowserFilterModel.cpp +++ b/Gems/ScriptCanvas/Code/Editor/Model/UnitTestBrowserFilterModel.cpp @@ -34,7 +34,7 @@ namespace ScriptCanvasEditor { setDynamicSortFilter(true); - m_showColumn.insert(AssetBrowserModel::m_column); + m_showColumn.insert(aznumeric_cast(AssetBrowserEntry::Column::DisplayName)); UnitTestWidgetNotificationBus::Handler::BusConnect(); diff --git a/Gems/ScriptCanvasPhysics/Code/Tests/ScriptCanvasPhysicsTest.cpp b/Gems/ScriptCanvasPhysics/Code/Tests/ScriptCanvasPhysicsTest.cpp index 07ccdc5011..63b43938e7 100644 --- a/Gems/ScriptCanvasPhysics/Code/Tests/ScriptCanvasPhysicsTest.cpp +++ b/Gems/ScriptCanvasPhysics/Code/Tests/ScriptCanvasPhysicsTest.cpp @@ -100,6 +100,9 @@ namespace ScriptCanvasPhysicsTests void DisableSimulationOfBody( [[maybe_unused]] AzPhysics::SceneHandle sceneHandle, [[maybe_unused]] AzPhysics::SimulatedBodyHandle bodyHandle) override {} + void RemoveJoint( + [[maybe_unused]]AzPhysics::SceneHandle sceneHandle, + [[maybe_unused]] AzPhysics::JointHandle jointHandle) override {} void SuppressCollisionEvents( [[maybe_unused]] AzPhysics::SceneHandle sceneHandle, [[maybe_unused]] const AzPhysics::SimulatedBodyHandle& bodyHandleA, @@ -148,6 +151,10 @@ namespace ScriptCanvasPhysicsTests MOCK_METHOD2(AddSimulatedBodies, AzPhysics::SimulatedBodyHandleList(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SimulatedBodyConfigurationList& simulatedBodyConfigs)); MOCK_METHOD2(GetSimulatedBodyFromHandle, AzPhysics::SimulatedBody* (AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle bodyHandle)); MOCK_METHOD2(GetSimulatedBodiesFromHandle, AzPhysics::SimulatedBodyList(AzPhysics::SceneHandle sceneHandle, const AzPhysics::SimulatedBodyHandleList& bodyHandles)); + MOCK_METHOD4(AddJoint, AzPhysics::JointHandle(AzPhysics::SceneHandle sceneHandle, const AzPhysics::JointConfiguration* jointConfig, + AzPhysics::SimulatedBodyHandle parentBody, AzPhysics::SimulatedBodyHandle childBody)); + MOCK_METHOD2( + GetJointFromHandle, AzPhysics::Joint*(AzPhysics::SceneHandle sceneHandle, AzPhysics::JointHandle jointHandle)); MOCK_CONST_METHOD1(GetGravity, AZ::Vector3(AzPhysics::SceneHandle sceneHandle)); MOCK_METHOD2(RegisterSceneSimulationFinishHandler, void(AzPhysics::SceneHandle sceneHandle, AzPhysics::SceneEvents::OnSceneSimulationFinishHandler& handler)); MOCK_CONST_METHOD2(GetLegacyBody, AzPhysics::SimulatedBody* (AzPhysics::SceneHandle sceneHandle, AzPhysics::SimulatedBodyHandle handle)); diff --git a/Gems/WhiteBox/Code/Source/EditorWhiteBoxComponentMode.cpp b/Gems/WhiteBox/Code/Source/EditorWhiteBoxComponentMode.cpp index 0772851cba..a3ec073e41 100644 --- a/Gems/WhiteBox/Code/Source/EditorWhiteBoxComponentMode.cpp +++ b/Gems/WhiteBox/Code/Source/EditorWhiteBoxComponentMode.cpp @@ -465,7 +465,7 @@ namespace WhiteBox AzToolsFramework::ViewportUi::ViewportUiRequestBus::EventResult( buttonId, AzToolsFramework::ViewportUi::DefaultViewportId, &AzToolsFramework::ViewportUi::ViewportUiRequestBus::Events::CreateClusterButton, clusterId, - AZStd::string::format("Icons/WhiteBox/%s.svg", iconName)); + AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s.svg", iconName)); return buttonId; } diff --git a/README.md b/README.md index 0c59837a62..7a9751529f 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ If you have the Git credential manager core or other credential helpers installe * Game Development with C++ * MSVC v142 - VS 2019 C++ x64/x86 * C++ 2019 redistributable update -* CMake 3.19.1 minimum: [https://cmake.org/files/LatestRelease/cmake-3.19.1-win64-x64.msi](https://cmake.org/files/LatestRelease/cmake-3.19.1-win64-x64.msi) +* CMake 3.20 minimum: [https://cmake.org/download/](https://cmake.org/download/) #### Optional @@ -105,7 +105,7 @@ If you have the Git credential manager core or other credential helpers installe 1. Install the following redistributables to the following: - Visual Studio and VC++ redistributable can be installed to any location - - CMake can be installed to any location, as long as it's available in the system path, otherwise it can be installed to: `<3rdParty Path>\CMake\3.19.1` + - CMake can be installed to any location, as long as it's available in the system path - WWise can be installed anywhere, but you will need to set an environment variable for CMake to detect it: `set LY_WWISE_INSTALL_PATH=` 1. Navigate into the repo folder, then download the python runtime with this command diff --git a/Registry/setregbuilder.assetprocessor.setreg b/Registry/setregbuilder.assetprocessor.setreg index 4be46a9d51..5dc6f42446 100644 --- a/Registry/setregbuilder.assetprocessor.setreg +++ b/Registry/setregbuilder.assetprocessor.setreg @@ -22,7 +22,8 @@ // members or entries will be recursively ignored as well. "Excludes": [ - "/Amazon/AzCore/Runtime" + "/Amazon/AzCore/Runtime", + "/Amazon/AzCore/Bootstrap/project_path" ] } } diff --git a/Templates/DefaultProject/Template/CMakeLists.txt b/Templates/DefaultProject/Template/CMakeLists.txt index 50e3a528e6..4dcfc5325b 100644 --- a/Templates/DefaultProject/Template/CMakeLists.txt +++ b/Templates/DefaultProject/Template/CMakeLists.txt @@ -12,7 +12,7 @@ # {END_LICENSE} if(NOT PROJECT_NAME) - cmake_minimum_required(VERSION 3.19) + cmake_minimum_required(VERSION 3.20) project(${Name} LANGUAGES C CXX VERSION 1.0.0.0 diff --git a/Tools/LyTestTools/ly_test_tools/__init__.py b/Tools/LyTestTools/ly_test_tools/__init__.py index d534f5e0d2..fe987deb67 100755 --- a/Tools/LyTestTools/ly_test_tools/__init__.py +++ b/Tools/LyTestTools/ly_test_tools/__init__.py @@ -28,6 +28,7 @@ WINDOWS = sys.platform.startswith('win') HOST_OS_PLATFORM = 'unknown' HOST_OS_EDITOR = 'unknown' HOST_OS_DEDICATED_SERVER = 'unknown' +HOST_OS_GENERIC_EXECUTABLE = 'unknown' LAUNCHERS = {} for launcher_option in ALL_LAUNCHER_OPTIONS: LAUNCHERS[launcher_option] = None diff --git a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake index f85048d13e..3908d21ecf 100644 --- a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake +++ b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake @@ -26,7 +26,7 @@ ly_associate_package(PACKAGE_NAME lz4-r128-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.21-rev1-multiplatform TARGETS azslc PACKAGE_HASH 772b7a2d9cc68aa1da4f0ee7db57ee1b4e7a8f20b81961fc5849af779582f4df) +ly_associate_package(PACKAGE_NAME azslc-1.7.22-rev1-multiplatform TARGETS azslc PACKAGE_HASH 71b4545d221d4fcd564ccc121c249a8f8f164bcc616faf146f926c3d5c78d527) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) diff --git a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake index 1a2cfa4049..19e71f726c 100644 --- a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake +++ b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake @@ -26,7 +26,7 @@ ly_associate_package(PACKAGE_NAME lz4-r128-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.21-rev1-multiplatform TARGETS azslc PACKAGE_HASH 772b7a2d9cc68aa1da4f0ee7db57ee1b4e7a8f20b81961fc5849af779582f4df) +ly_associate_package(PACKAGE_NAME azslc-1.7.22-rev1-multiplatform TARGETS azslc PACKAGE_HASH 71b4545d221d4fcd564ccc121c249a8f8f164bcc616faf146f926c3d5c78d527) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) diff --git a/cmake/LYTestWrappers.cmake b/cmake/LYTestWrappers.cmake index b4d6fe308e..d925015c5e 100644 --- a/cmake/LYTestWrappers.cmake +++ b/cmake/LYTestWrappers.cmake @@ -71,6 +71,7 @@ endfunction() #! ly_add_test: Adds a new RUN_TEST using for the specified target using the supplied command # # \arg:NAME - Name to for the test run target +# \arg:PARENT_NAME(optional) - Name of the parent test run target (if this is a subsequent call to specify a suite) # \arg:TEST_REQUIRES(optional) - List of system resources that are required to run this test. # Only available option is "gpu" # \arg:TEST_SUITE(optional) - "smoke" or "periodic" or "sandbox" - prevents the test from running normally @@ -94,7 +95,7 @@ endfunction() # sets LY_ADDED_TEST_NAME to the fully qualified name of the test, in parent scope function(ly_add_test) set(options EXCLUDE_TEST_RUN_TARGET_FROM_IDE) - set(one_value_args NAME TEST_LIBRARY TEST_SUITE TIMEOUT) + set(one_value_args NAME PARENT_NAME TEST_LIBRARY TEST_SUITE TIMEOUT) set(multi_value_args TEST_REQUIRES TEST_COMMAND NON_IDE_PARAMS RUNTIME_DEPENDENCIES COMPONENT LABELS) # note that we dont use TEST_LIBRARY here, but PAL files might so do not remove! @@ -151,12 +152,6 @@ function(ly_add_test) set(LY_ADDED_TEST_NAME ${qualified_test_run_name_with_suite}::TEST_RUN) set(LY_ADDED_TEST_NAME ${LY_ADDED_TEST_NAME} PARENT_SCOPE) - # Store the test so we can walk through all of them in LYTestImpactFramework.cmake - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS ${LY_ADDED_TEST_NAME}) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${LY_ADDED_TEST_NAME}_TEST_NAME ${ly_add_test_NAME}) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${LY_ADDED_TEST_NAME}_TEST_SUITE ${ly_add_test_TEST_SUITE}) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${LY_ADDED_TEST_NAME}_TEST_LIBRARY ${ly_add_test_TEST_LIBRARY}) - set(final_labels SUITE_${ly_add_test_TEST_SUITE}) if (ly_add_test_TEST_REQUIRES) @@ -247,6 +242,24 @@ function(ly_add_test) endif() + if(NOT ly_add_test_PARENT_NAME) + set(test_target ${ly_add_test_NAME}) + else() + set(test_target ${ly_add_test_PARENT_NAME}) + endif() + + # Check to see whether or not this test target has been stored in the global list for walking by the test impact analysis framework + get_property(all_tests GLOBAL PROPERTY LY_ALL_TESTS) + if(NOT "${test_target}" IN_LIST all_tests) + # This is the first reference to this test target so add it to the global list + set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS ${test_target}) + set_property(GLOBAL PROPERTY LY_ALL_TESTS_${test_target}_TEST_LIBRARY ${ly_add_test_TEST_LIBRARY}) + endif() + # Add the test suite and timeout value to the test target params + set(LY_TEST_PARAMS "${LY_TEST_PARAMS}#${ly_add_test_TEST_SUITE}") + set(LY_TEST_PARAMS "${LY_TEST_PARAMS}#${ly_add_test_TIMEOUT}") + # Store the params for this test target + set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${test_target}_PARAMS ${LY_TEST_PARAMS}) endfunction() #! ly_add_pytest: registers target PyTest-based test with CTest @@ -288,8 +301,12 @@ function(ly_add_pytest) string(REPLACE "::" "_" pytest_report_directory "${PYTEST_XML_OUTPUT_DIR}/${ly_add_pytest_NAME}.xml") + # Add the script path to the test target params + set(LY_TEST_PARAMS "${ly_add_pytest_PATH}") + ly_add_test( NAME ${ly_add_pytest_NAME} + PARENT_NAME ${ly_add_pytest_NAME} TEST_SUITE ${ly_add_pytest_TEST_SUITE} LABELS FRAMEWORK_pytest TEST_COMMAND ${LY_PYTEST_EXECUTABLE} ${ly_add_pytest_PATH} ${ly_add_pytest_EXTRA_ARGS} --junitxml=${pytest_report_directory} ${custom_marks_args} @@ -298,73 +315,8 @@ function(ly_add_pytest) ${ly_add_pytest_UNPARSED_ARGUMENTS} ) + set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${ly_add_pytest_NAME}_SCRIPT_PATH ${ly_add_pytest_PATH}) set_tests_properties(${LY_ADDED_TEST_NAME} PROPERTIES RUN_SERIAL "${ly_add_pytest_TEST_SERIAL}") - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${LY_ADDED_TEST_NAME}_SCRIPT_PATH ${ly_add_pytest_PATH}) -endfunction() - -#! ly_add_editor_python_test: registers target Editor Python Bindings test with CTest -# -# \arg:NAME name of the test-module to register with CTest -# \arg:PATH path to the file (or dir) containing Editor Python Bindings-based tests -# \arg:TEST_PROJECT Name of the project to be set before running the test -# \arg:TEST_SUITE name of the test suite to register with CTest -# \arg:TEST_SERIAL (bool) disable parallel execution alongside other test modules, important when this test depends on shared resources or environment state -# \arg:TEST_REQUIRES (optional) list of system resources needed by the tests in this module. Used to filter out execution when those system resources are not available. For example, 'gpu' -# \arg:RUNTIME_DEPENDENCIES (optional) - List of additional runtime dependencies required by this test. -# "Editor" and "EditorPythonBindings" gem are automatically included as dependencies. -# \arg:COMPONENT (optional) - Scope of the feature area that the test belongs to (eg. physics, graphics, etc.). -# \arg:TIMEOUT (optional) The timeout in seconds for the module. If not set, will have its timeout set by ly_add_test to the default timeout. -function(ly_add_editor_python_test) - if(NOT PAL_TRAIT_TEST_PYTEST_SUPPORTED) - return() - endif() - - set(options TEST_SERIAL) - set(oneValueArgs NAME PATH TEST_SUITE TEST_PROJECT TIMEOUT) - set(multiValueArgs TEST_REQUIRES RUNTIME_DEPENDENCIES COMPONENT) - - cmake_parse_arguments(ly_add_editor_python_test "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - set(executable_target $) - - if(NOT TARGET Legacy::Editor) - message(FATAL_ERROR "Legacy::Editor was not recognized as a valid target") - endif() - - if(NOT ly_add_editor_python_test_PATH) - message(FATAL_ERROR "Must supply a value for PATH to tests") - endif() - - if(NOT ly_add_editor_python_test_TEST_SUITE) - message(FATAL_ERROR "Must supply a value for TEST_SUITE") - endif() - - file(REAL_PATH ${ly_add_editor_python_test_TEST_PROJECT} project_real_path BASE_DIRECTORY ${LY_ROOT_FOLDER}) - - # Run test via the run_epbtest.cmake script. - # Parameters used are explained in run_epbtest.cmake. - ly_add_test( - NAME ${ly_add_editor_python_test_NAME} - TEST_REQUIRES ${ly_add_editor_python_test_TEST_REQUIRES} - TEST_COMMAND ${CMAKE_COMMAND} - -DCMD_ARG_TEST_PROJECT=${project_real_path} - -DCMD_ARG_EDITOR=$ - -DCMD_ARG_PYTHON_SCRIPT=${ly_add_editor_python_test_PATH} - -DPLATFORM=${PAL_PLATFORM_NAME} - -P ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/run_epbtest.cmake - RUNTIME_DEPENDENCIES - ${ly_add_editor_python_test_RUNTIME_DEPENDENCIES} - Gem::EditorPythonBindings.Editor - Legacy::Editor - TEST_SUITE ${ly_add_editor_python_test_TEST_SUITE} - LABELS FRAMEWORK_pytest - TEST_LIBRARY pytest_editor - TIMEOUT ${ly_add_editor_python_test_TIMEOUT} - COMPONENT ${ly_add_editor_python_test_COMPONENT} - ) - - set_tests_properties(${LY_ADDED_TEST_NAME} PROPERTIES RUN_SERIAL "${ly_add_editor_python_test_TEST_SERIAL}") - set_property(GLOBAL APPEND PROPERTY LY_ALL_TESTS_${LY_ADDED_TEST_NAME}_SCRIPT_PATH ${ly_add_editor_python_test_PATH}) endfunction() #! ly_add_googletest: Adds a new RUN_TEST using for the specified target using the supplied command or fallback to running @@ -431,8 +383,16 @@ function(ly_add_googletest) set(full_test_command $ $ AzRunUnitTests) # Add AzTestRunner as a build dependency ly_add_dependencies(${build_target} AZ::AzTestRunner) + # Start the test target params and dd the command runner command + # Ideally, we would populate the full command procedurally but the generator expressions won't be expanded by the time we need this data + set(LY_TEST_PARAMS "AzRunUnitTests") else() set(full_test_command ${ly_add_googletest_TEST_COMMAND}) + # Remove the generator expressions so we are left with the argument(s) required to run unit tests for executable targets + string(REPLACE ";" "" stripped_test_command ${full_test_command}) + string(GENEX_STRIP ${stripped_test_command} stripped_test_command) + # Start the test target params and dd the command runner command + set(LY_TEST_PARAMS "${stripped_test_command}") endif() string(REPLACE "::" "_" report_directory "${GTEST_XML_OUTPUT_DIR}/${ly_add_googletest_NAME}.xml") @@ -440,6 +400,7 @@ function(ly_add_googletest) # Invoke the lower level ly_add_test command to add the actual ctest and setup the test labels to add_dependencies on the target ly_add_test( NAME ${ly_add_googletest_NAME} + PARENT_NAME ${target_name} TEST_SUITE ${ly_add_googletest_TEST_SUITE} LABELS FRAMEWORK_googletest TEST_COMMAND ${full_test_command} --gtest_output=xml:${report_directory} ${LY_GOOGLETEST_EXTRA_PARAMS} @@ -514,12 +475,22 @@ function(ly_add_googlebenchmark) # If command is not supplied attempts, uses the AzTestRunner to run googlebenchmarks on the supplied TARGET set(full_test_command $ $ AzRunBenchmarks ${output_format_args}) + # Start the test target params and dd the command runner command + # Ideally, we would populate the full command procedurally but the generator expressions won't be expanded by the time we need this data + set(LY_TEST_PARAMS "AzRunUnitTests") else() set(full_test_command ${ly_add_googlebenchmark_TEST_COMMAND}) + # Remove the generator expressions so we are left with the argument(s) required to run unit tests for executable targets + string(REPLACE ";" "" stripped_test_command ${full_test_command}) + string(GENEX_STRIP ${stripped_test_command} stripped_test_command) + # Start the test target params and dd the command runner command + set(LY_TEST_PARAMS "${stripped_test_command}") endif() + # Set the name of the current test target for storage in the global list ly_add_test( NAME ${ly_add_googlebenchmark_NAME} + PARENT_NAME ${ly_add_googlebenchmark_NAME} TEST_REQUIRES ${ly_add_googlebenchmark_TEST_REQUIRES} TEST_COMMAND ${full_test_command} ${LY_GOOGLETEST_EXTRA_PARAMS} TEST_SUITE "benchmark" @@ -531,6 +502,5 @@ function(ly_add_googlebenchmark) AZ::AzTestRunner COMPONENT ${ly_add_googlebenchmark_COMPONENT} ) - endfunction() diff --git a/cmake/Platform/Android/PAL_android.cmake b/cmake/Platform/Android/PAL_android.cmake index 5f35767f98..dd61e35e53 100644 --- a/cmake/Platform/Android/PAL_android.cmake +++ b/cmake/Platform/Android/PAL_android.cmake @@ -24,6 +24,7 @@ ly_set(PAL_TRAIT_BUILD_CPACK_SUPPORTED FALSE) # Test library support ly_set(PAL_TRAIT_TEST_GOOGLE_TEST_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_GOOGLE_BENCHMARK_SUPPORTED FALSE) +ly_set(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_PYTEST_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_TARGET_TYPE MODULE) diff --git a/cmake/Platform/Linux/PAL_linux.cmake b/cmake/Platform/Linux/PAL_linux.cmake index 40b73adcec..c15f2bada9 100644 --- a/cmake/Platform/Linux/PAL_linux.cmake +++ b/cmake/Platform/Linux/PAL_linux.cmake @@ -24,6 +24,7 @@ ly_set(PAL_TRAIT_BUILD_CPACK_SUPPORTED FALSE) # Test library support ly_set(PAL_TRAIT_TEST_GOOGLE_TEST_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_GOOGLE_BENCHMARK_SUPPORTED TRUE) +ly_set(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_PYTEST_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_TARGET_TYPE MODULE) diff --git a/cmake/Platform/Mac/PAL_mac.cmake b/cmake/Platform/Mac/PAL_mac.cmake index 988d36af14..f49578a83a 100644 --- a/cmake/Platform/Mac/PAL_mac.cmake +++ b/cmake/Platform/Mac/PAL_mac.cmake @@ -24,6 +24,7 @@ ly_set(PAL_TRAIT_BUILD_CPACK_SUPPORTED FALSE) # Test library support ly_set(PAL_TRAIT_TEST_GOOGLE_TEST_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_GOOGLE_BENCHMARK_SUPPORTED TRUE) +ly_set(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_PYTEST_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_TARGET_TYPE MODULE) diff --git a/cmake/Platform/Windows/PAL_windows.cmake b/cmake/Platform/Windows/PAL_windows.cmake index ddcc3a27c7..fbf65db63f 100644 --- a/cmake/Platform/Windows/PAL_windows.cmake +++ b/cmake/Platform/Windows/PAL_windows.cmake @@ -24,6 +24,7 @@ ly_set(PAL_TRAIT_BUILD_CPACK_SUPPORTED TRUE) # Test library support ly_set(PAL_TRAIT_TEST_GOOGLE_TEST_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_GOOGLE_BENCHMARK_SUPPORTED TRUE) +ly_set(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_PYTEST_SUPPORTED TRUE) ly_set(PAL_TRAIT_TEST_TARGET_TYPE MODULE) diff --git a/cmake/Platform/Windows/Packaging/Bootstrapper.wxs b/cmake/Platform/Windows/Packaging/Bootstrapper.wxs index 55e8a8cd95..fd8aa68b77 100644 --- a/cmake/Platform/Windows/Packaging/Bootstrapper.wxs +++ b/cmake/Platform/Windows/Packaging/Bootstrapper.wxs @@ -22,6 +22,8 @@ @@ -29,6 +31,8 @@ diff --git a/cmake/Platform/Windows/Packaging/BootstrapperTheme.wxl.in b/cmake/Platform/Windows/Packaging/BootstrapperTheme.wxl.in new file mode 100644 index 0000000000..fe125cfdfe --- /dev/null +++ b/cmake/Platform/Windows/Packaging/BootstrapperTheme.wxl.in @@ -0,0 +1,70 @@ + + + + + [WixBundleName] Setup + [WixBundleName] + Version [WixBundleVersion] + Are you sure you want to cancel? + + + Welcome + +Setup will install [WixBundleName] on your computer. Click install to continue, options to set the install directory or Close to exit. + + [WixBundleName] <a href="#">license terms</a>. + I &agree to the license terms and conditions + &Options + &Install + &Close + + + Setup Options + Install location: + &Browse + &OK + &Cancel + + + Modify Setup + &Repair + &Uninstall + &Close + + + Setup Progress + Processing: + Initializing... + &Cancel + + + Setup Successful + Installation Successfully Completed + Repair Successfully Completed + Uninstall Successfully Completed + &Launch + &Close + + + Setup Failed + Setup Failed + Uninstall Failed + Repair Failed + +One or more issues caused the setup to fail. Please fix the issues and then retry setup. For more information see the <a href="#">log file</a>. + + &Close + + + Setup Help + +/install | /repair | /uninstall | /layout [directory] - installs, repairs, uninstalls or creates a complete local copy of the bundle in directory. Install is the default. + +/passive | /quiet - displays minimal UI with no prompts or displays no UI and no prompts. By default UI and all prompts are displayed. + +/log log.txt - logs to a specific file. By default a log file is created in %TEMP%. + + &Close + diff --git a/cmake/Platform/Windows/Packaging/BootstrapperTheme.xml.in b/cmake/Platform/Windows/Packaging/BootstrapperTheme.xml.in new file mode 100644 index 0000000000..61b338cc13 --- /dev/null +++ b/cmake/Platform/Windows/Packaging/BootstrapperTheme.xml.in @@ -0,0 +1,87 @@ + + + + #(loc.WindowTitle) + + Segoe UI + Segoe UI + Segoe UI + Segoe UI + + + + #(loc.Title) + + + + @WIX_THEME_INSTALL_LICENSE_ELEMENT@ + + #(loc.InstallAcceptCheckbox) + + + + + + + + #(loc.OptionsHeader) + + #(loc.OptionsLocationLabel) + + + + + + + + + + #(loc.ModifyHeader) + + + + + + + + + #(loc.ProgressHeader) + + #(loc.ProgressLabel) + #(loc.OverallProgressPackageText) + + + + + + + + #(loc.SuccessHeader) + #(loc.SuccessInstallHeader) + #(loc.SuccessRepairHeader) + #(loc.SuccessUninstallHeader) + + + + + + + + #(loc.FailureHeader) + #(loc.FailureInstallHeader) + #(loc.FailureUninstallHeader) + #(loc.FailureRepairHeader) + + #(loc.FailureHyperlinkLogText) + + + + + + + + #(loc.HelpHeader) + #(loc.HelpText) + + + diff --git a/cmake/Platform/Windows/PackagingPostBuild.cmake b/cmake/Platform/Windows/PackagingPostBuild.cmake index 89b3efb44b..1dcbedcba7 100644 --- a/cmake/Platform/Windows/PackagingPostBuild.cmake +++ b/cmake/Platform/Windows/PackagingPostBuild.cmake @@ -25,6 +25,7 @@ set(_ext_flags ) set(_addtional_defines + -dCPACK_BOOTSTRAP_THEME_FILE=${CPACK_BINARY_DIR}/BootstrapperTheme -dCPACK_BOOTSTRAP_UPGRADE_GUID=${CPACK_WIX_BOOTSTRAP_UPGRADE_GUID} -dCPACK_DOWNLOAD_SITE=${CPACK_DOWNLOAD_SITE} -dCPACK_LOCAL_INSTALLER_DIR=${_cpack_wix_out_dir} diff --git a/cmake/Platform/Windows/Packaging_windows.cmake b/cmake/Platform/Windows/Packaging_windows.cmake index 2fd281ad51..aec0edeee2 100644 --- a/cmake/Platform/Windows/Packaging_windows.cmake +++ b/cmake/Platform/Windows/Packaging_windows.cmake @@ -92,6 +92,28 @@ set(CPACK_WIX_EXTENSIONS set(_embed_artifacts "yes") if(LY_INSTALLER_DOWNLOAD_URL) + + if(LY_INSTALLER_LICENSE_URL) + set(WIX_THEME_INSTALL_LICENSE_ELEMENT + "#(loc.InstallLicenseLinkText)" + ) + else() + set(WIX_THEME_INSTALL_LICENSE_ELEMENT + "" + ) + endif() + + configure_file( + "${CPACK_SOURCE_DIR}/Platform/Windows/Packaging/BootstrapperTheme.xml.in" + "${CPACK_BINARY_DIR}/BootstrapperTheme.xml" + @ONLY + ) + configure_file( + "${CPACK_SOURCE_DIR}/Platform/Windows/Packaging/BootstrapperTheme.wxl.in" + "${CPACK_BINARY_DIR}/BootstrapperTheme.wxl" + @ONLY + ) + set(_embed_artifacts "no") # the bootstrapper will at the very least need a different upgrade guid diff --git a/cmake/Platform/Windows/platform_windows_files.cmake b/cmake/Platform/Windows/platform_windows_files.cmake index 3ce53fbcea..b3cdc8a4ff 100644 --- a/cmake/Platform/Windows/platform_windows_files.cmake +++ b/cmake/Platform/Windows/platform_windows_files.cmake @@ -26,6 +26,8 @@ set(FILES Packaging_windows.cmake PackagingPostBuild.cmake Packaging/Bootstrapper.wxs + Packaging/BootstrapperTheme.wxl.in + Packaging/BootstrapperTheme.xml.in Packaging/Shortcuts.wxs Packaging/Template.wxs.in ) diff --git a/cmake/Platform/iOS/PAL_ios.cmake b/cmake/Platform/iOS/PAL_ios.cmake index a6828a1bc7..981bb9cab1 100644 --- a/cmake/Platform/iOS/PAL_ios.cmake +++ b/cmake/Platform/iOS/PAL_ios.cmake @@ -24,6 +24,7 @@ ly_set(PAL_TRAIT_BUILD_CPACK_SUPPORTED FALSE) # Test library support ly_set(PAL_TRAIT_TEST_GOOGLE_TEST_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_GOOGLE_BENCHMARK_SUPPORTED FALSE) +ly_set(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_PYTEST_SUPPORTED FALSE) ly_set(PAL_TRAIT_TEST_TARGET_TYPE MODULE) diff --git a/cmake/TestImpactFramework/ConsoleFrontendConfig.in b/cmake/TestImpactFramework/ConsoleFrontendConfig.in index 2371d7fb7f..9338672fbd 100644 --- a/cmake/TestImpactFramework/ConsoleFrontendConfig.in +++ b/cmake/TestImpactFramework/ConsoleFrontendConfig.in @@ -1,110 +1,208 @@ -[path.configuration] -repo_dir = "${repo_dir}" -working_dir = "${working_dir}" -bin_dir = "${runtime_bin_dir}" -tests_dir = "${tests_dir}" -temp_dir = "${temp_dir}" -target_mappings_dir = "${source_target_mapping_dir}" -test_type_dir = "${test_type_dir}" -dependencies_dir = "${target_dependency_dir}" - -[sourcetree.configuration.filters.autogen] -# E.g. matches input /Foo/{Bar}.FooBar.xml with output /Baz/{Bar}.BazBar.cpp -input_output_pairer = "(.*)\\..*" -[sourcetree.configuration.filters.autogen.input] -exclude_filter = [".jinja"] -[sourcetree.configuration.filters.source] -exclude_filter = [".cmake"] -[sourcetree.configuration.testtype.enumerated] -file = "All.tests" -# The table to read from the test enumeration file that contains the test targets -target_table = "google.test" -[sourcetree.configuration.dependency] -# E.g. matches WhiteBox.Editor.Static\n(Gem::WhiteBox.Editor.Static) or WhiteBox.Editor.Static -target_dependency_file_matcher = "target\\.(.*)\\.(dependers)?" -# E.g. matches target.WhiteBox.Editor.Static (for dependency) target.WhiteBox.Editor.Static.dependers (for dependers) -target_vertex_matcher = "(?:(.*)\\n|(.*)" - -[spartia.configuration] -test_impact_Data_file = "TestImpactData.spartia" -test_run_coverage_file = "{test_dir}\\{test_target}.coverage.xml" -test_run_results_file = "{test_dir}\\{test_target}.results.xml" -test_enumeration_file = "{temp_dir}\\{test_target}.enum" -test_shard_selection_file = "{temp_dir}\\{test_target}.filter.{shard_id}" -exclude_filter = [ -{ target = "AssetBundler.Tests", tests = ["*"] }, -{ target = "AssetProcessor.Tests", tests = ["*"] }, -{ target = "CryRenderD3D11.Tests", tests = ["*"] }, -{ target = "CryRenderD3D12.Tests", tests = ["*"] }, -{ target = "LyzardApplicationDescriptors.Tests", tests = ["*"] }, -{ target = "EMotionFX.Editor.Tests", tests = ["UIFixture.*", "SimulatedObjectModelTestsFixture.*", "TestParametersFixture.*", "CanSeeJointsFixture.*", "LODSkinnedMeshFixtureTests/LODSkinnedMeshFixture.CheckLODLevels/*"] }, -{ target = "EMotionFX.Tests", tests = ["UIFixture.*", "SimulatedObjectModelTestsFixture.*", "TestParametersFixture.*", "CanSeeJointsFixture.*"] }, -{ target = "AzCore.Tests", tests = ["AllocatorsTestFixtureLeakDetectionDeathTest_SKIPCODECOVERAGE.AllocatorLeak"] }, -] -[spartia.configuration.shard] -# Long tests that will be sharded -include_filter = [ -{ target = "AzCore.Tests", policy = "fixture_contiguous" }, -{ target = "AzToolsFramework.Tests", policy = "fixture_contiguous" }, -{ target = "Framework.Tests", policy = "test_interleaved" }, -{ target = "LmbrCentral.Editor.Tests", policy = "test_interleaved" }, -{ target = "EditorLib.Tests", policy = "test_interleaved" }, -{ target = "PhysX.Tests", policy = "test_interleaved" }, -{ target = "Atom_RPI.Tests", policy = "test_interleaved" }, -{ target = "Atom_RHI.Tests", policy = "test_interleaved" }, -{ target = "AzManipulatorFramework.Tests", policy = "test_interleaved" }, -{ target = "WhiteBox.Editor.Tests", policy = "test_interleaved" }, -{ target = "AzManipulatorTestFramework.Tests", policy = "test_interleaved" }, -{ target = "AtomCore.Tests", policy = "test_interleaved" }, -{ target = "ImageProcessingAtom.Editor.Tests", policy = "test_interleaved" }, -{ target = "EditorPythonBindings.Tests", policy = "test_interleaved" }, -{ target = "Atom_Utils.Tests", policy = "test_interleaved" }, -{ target = "AudioEngineWwise.Editor.Tests", policy = "test_interleaved" }, -{ target = "Multiplayer.Tests", policy = "test_interleaved" }, -{ target = "LmbrCentral.Tests", policy = "test_interleaved" }, -{ target = "LyMetricsShared.Tests", policy = "fixture_contiguous" }, -{ target = "PhysX.Editor.Tests", policy = "test_interleaved" }, -{ target = "ComponentEntityEditorPlugin.Tests", policy = "test_interleaved" }, -{ target = "DeltaCataloger.Tests", policy = "test_interleaved" }, -{ target = "GradientSignal.Tests", policy = "test_interleaved" }, -{ target = "LyShine.Tests", policy = "test_interleaved" }, -{ target = "EMotionFX.Editor.Tests", policy = "test_interleaved" }, -{ target = "EMotionFX.Tests", policy = "test_interleaved" }, -{ target = "CrySystem.Tests", policy = "test_interleaved" }, -] -[spartia.configuration.instrumentation] -abs_bin = "${instrumentation_bin}" -[spartia.configuration.instrumentation.errors] -# AzCppCoverage error codes -incorrect_args = -1618178468 -[spartia.configuration.instrumentation.test_coverage] -args = "--export_type cobertura:\"{test_run_coverage_file}\"" -[spartia.configuration.instrumentation.test_selection] -args = "--gtest_filter={test_selection}" -[spartia.configuration.instrumentation.test_enumeration] -args = "--gtest_list_tests" -[spartia.configuration.instrumentation.test_results] -args = "--gtest_output=xml:\"{test_run_results_file}\"" -[spartia.configuration.instrumentation.test_results.errors] -test_success = 0 -test_failures = 1 -[spartia.configuration.instrumentation.binary_type.dynlib] -abs_bin = "{bin_dir}\\AzTestRunner.exe" -args = "\"{bin_dir}\\{test_target}.dll\" AzRunUnitTests" -[spartia.configuration.instrumentation.binary_type.dynlib.test_enumeration] -args = "--stdout_to_file \"{test_enumeration_file}\" {test_enumeration}" -[spartia.configuration.instrumentation.binary_type.dynlib.test_shard_selection] -args = "--args_from_file \"{test_shard_selection_file}\"" -[spartia.configuration.instrumentation.binary_type.dynlib.errors] -# AzTestRunner error codes -failed_to_find_target_bin = 102 -incorrect_args = 101 -known_errors = [ 103, 104] -[spartia.configuration.instrumentation.binary_type.executable] -abs_bin = "{bin_dir}\\{test_target}.exe" -[spartia.configuration.instrumentation.binary_type.executable.test_enumeration] -args = "--stdout_to_file \"{test_enumeration_file}\" {test_enumeration}" -[spartia.configuration.instrumentation.binary_type.executable.test_shard_selection] -args = "--args_from_file \"{test_shard_selection_file}\"" -[spartia.configuration.test_run.seed] -instrumentation_args = "--modules \"{bin_dir}\" --excluded_modules \"{binary_type.dynlib.abs_bin}\" --sources \"{repo_dir}\" --no_breakpoints {test_coverage} -- " \ No newline at end of file +{ + "meta": { + "platform": "${platform}", + "timestamp": "${timestamp}" + }, + "jenkins": { + "pipeline_of_truth" : [ + "nightly-incremental", + "nightly-clean" + ], + "use_test_impact_analysis": ${use_tiaf} + }, + "repo": { + "root": "${repo_dir}", + "tiaf_bin": "${tiaf_bin}" + }, + "workspace": { + "temp": { + "root": "${temp_dir}", + "relative_paths": { + "artifact_dir": "RuntimeArtifact" + } + }, + "active": { + "root": "${active_dir}", + "relative_paths": { + "test_impact_data_files": { + "main": "TestImpactData.main.spartia", + "periodic": "TestImpactData.periodic.spartia", + "sandbox": "TestImpactData.sandbox.spartia" + }, + "enumeration_cache_dir": "EnumerationCache", + "last_build_target_list_file": "LastRunBuildTargets.json" + } + }, + "historic": { + "root": "${historic_dir}", + "relative_paths": { + "last_run_hash_file": "last_run.hash", + "last_build_target_list_file": "LastRunBuildTargets.json" + } + } + }, + "artifacts": { + "static": { + "build_target_descriptor": { + "dir": "${source_target_mapping_dir}", + "target_sources": { + "static": { + "include_filters": [ + ".h", ".hpp", ".hxx", ".inl", ".c", ".cpp", ".cxx" + ] + }, + "autogen": { + "input_output_pairer": "(.*)\\..*", + "input": { + "include_filters": [ + ".xml" + ] + } + } + } + }, + "dependency_graph_data": { + "dir": "${target_dependency_dir}", + "matchers": { + "target_dependency_file": "target\\.(.*)\\.(dependers)?", + "target_vertex": "(?:(.*)\\n|(.*)" + } + }, + "test_target_meta": { + "file": "${test_target_type_file}" + } + } + }, + "test_engine": { + "test_runner": { + "bin": "${test_runner_bin}" + }, + "instrumentation": { + "bin": "${instrumentation_bin}" + } + }, + "target": { + "dir": "${bin_dir}", + "exclude": [ + + ], + "shard": [ + { + "policy": "fixture_contiguous", + "target": "AzCore.Tests" + }, + { + "policy": "fixture_contiguous", + "target": "AzToolsFramework.Tests" + }, + { + "policy": "test_interleaved", + "target": "Framework.Tests" + }, + { + "policy": "test_interleaved", + "target": "LmbrCentral.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "EditorLib.Tests" + }, + { + "policy": "test_interleaved", + "target": "PhysX.Tests" + }, + { + "policy": "test_interleaved", + "target": "ImageProcessing.Tests" + }, + { + "policy": "test_interleaved", + "target": "Atom_RPI.Tests" + }, + { + "policy": "test_interleaved", + "target": "Atom_RHI.Tests" + }, + { + "policy": "test_interleaved", + "target": "AzManipulatorFramework.Tests" + }, + { + "policy": "test_interleaved", + "target": "WhiteBox.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "ImageProcessing.Tests" + }, + { + "policy": "test_interleaved", + "target": "AzManipulatorTestFramework.Tests" + }, + { + "policy": "test_interleaved", + "target": "AtomCore.Tests" + }, + { + "policy": "test_interleaved", + "target": "ImageProcessingAtom.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "EditorPythonBindings.Tests" + }, + { + "policy": "test_interleaved", + "target": "Atom_Utils.Tests" + }, + { + "policy": "test_interleaved", + "target": "AudioEngineWwise.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "Multiplayer.Tests" + }, + { + "policy": "test_interleaved", + "target": "LmbrCentral.Tests" + }, + { + "policy": "fixture_contiguous", + "target": "LyMetricsShared.Tests" + }, + { + "policy": "test_interleaved", + "target": "PhysX.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "ComponentEntityEditorPlugin.Tests" + }, + { + "policy": "test_interleaved", + "target": "DeltaCataloger.Tests" + }, + { + "policy": "test_interleaved", + "target": "GradientSignal.Tests" + }, + { + "policy": "test_interleaved", + "target": "LyShine.Tests" + }, + { + "policy": "test_interleaved", + "target": "EMotionFX.Editor.Tests" + }, + { + "policy": "test_interleaved", + "target": "EMotionFX.Tests" + }, + { + "policy": "test_interleaved", + "target": "CrySystem.Tests" + } + ] + } +} diff --git a/cmake/TestImpactFramework/EnumeratedTests.in b/cmake/TestImpactFramework/EnumeratedTests.in index acd33fb661..02eb648f5f 100644 --- a/cmake/TestImpactFramework/EnumeratedTests.in +++ b/cmake/TestImpactFramework/EnumeratedTests.in @@ -1,23 +1,31 @@ -#Lumberyard enumerated tests -[google] -[google.test] -tests = [ -${google_tests} -] -[google.benchmark] -tests = [ +{ + "google": { + "benchmark": { + "tests": [ ${google_benchmarks} -] -[python] -[python.test] -tests = [ -${python_tests} -] -[python.editor] -tests = [ + ] + }, + "test": { + "tests": [ +${google_tests} + ] + } + }, + "python": { + "editor": { + "tests": [ ${python_editor_tests} -] -[unknown] -tests = [ + ] + }, + "test": { + "tests": [ +${python_tests} + ] + } + }, + "unknown": { + "tests": [ ${unknown_tests} -] \ No newline at end of file + ] + } +} \ No newline at end of file diff --git a/cmake/TestImpactFramework/LYTestImpactFramework.cmake b/cmake/TestImpactFramework/LYTestImpactFramework.cmake index d46b16bca5..661a066477 100644 --- a/cmake/TestImpactFramework/LYTestImpactFramework.cmake +++ b/cmake/TestImpactFramework/LYTestImpactFramework.cmake @@ -15,6 +15,9 @@ option(LY_TEST_IMPACT_ACTIVE "Enable test impact framework" OFF) # Path to test instrumentation binary option(LY_TEST_IMPACT_INSTRUMENTATION_BIN "Path to test impact framework instrumentation binary" OFF) +# Name of test impact framework console static library target +set(LY_TEST_IMPACT_CONSOLE_STATIC_TARGET "TestImpact.Frontend.Console.Static") + # Name of test impact framework console target set(LY_TEST_IMPACT_CONSOLE_TARGET "TestImpact.Frontend.Console") @@ -25,7 +28,7 @@ set(LY_TEST_IMPACT_WORKING_DIR "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/TestImpactFram set(LY_TEST_IMPACT_TEMP_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Temp") # Directory for static artifacts produced as part of the build system generation process -set(LY_TEST_IMPACT_ARTIFACT_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Artefact") +set(LY_TEST_IMPACT_ARTIFACT_DIR "${LY_TEST_IMPACT_WORKING_DIR}/Artifact") # Directory for source to build target mappings set(LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/Mapping") @@ -33,11 +36,8 @@ set(LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/Map # Directory for build target dependency/depender graphs set(LY_TEST_IMPACT_TARGET_DEPENDENCY_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/Dependency") -# Directory for test type enumeration files -set(LY_TEST_IMPACT_TEST_TYPE_DIR "${LY_TEST_IMPACT_ARTIFACT_DIR}/TestType") - # Master test enumeration file for all test types -set(LY_TEST_IMPACT_TEST_TYPE_FILE "${LY_TEST_IMPACT_TEST_TYPE_DIR}/All.tests") +set(LY_TEST_IMPACT_TEST_TYPE_FILE "${LY_TEST_IMPACT_ARTIFACT_DIR}/TestType/All.tests") #! ly_test_impact_rebase_file_to_repo_root: rebases the relative and/or absolute path to be relative to repo root directory and places the resulting path in quotes. # @@ -77,73 +77,139 @@ function(ly_test_impact_rebase_files_to_repo_root INPUT_FILES OUTPUT_FILES RELAT set(${OUTPUT_FILES} ${rebased_files} PARENT_SCOPE) endfunction() -#! ly_test_impact_get_target_type_string: gets the target type string (either executable, dynalib or unknown) for the specified target. +#! ly_test_impact_get_test_launch_method: gets the launch method (either standalone or testrunner) for the specified target. # # \arg:TARGET_NAME name of the target -# \arg:TARGET_TYPE the type string for the specified target -function(ly_test_impact_get_target_type_string TARGET_NAME TARGET_TYPE) - # Get the test impact framework-friendly target type string +# \arg:LAUNCH_METHOD the type string for the specified target +function(ly_test_impact_get_test_launch_method TARGET_NAME LAUNCH_METHOD) + # Get the test impact framework-friendly launch method string get_target_property(target_type ${TARGET_NAME} TYPE) if("${target_type}" STREQUAL "SHARED_LIBRARY" OR "${target_type}" STREQUAL "MODULE_LIBRARY") - set(${TARGET_TYPE} "dynlib" PARENT_SCOPE) + set(${LAUNCH_METHOD} "test_runner" PARENT_SCOPE) elseif("${target_type}" STREQUAL "EXECUTABLE") - set(${TARGET_TYPE} "executable" PARENT_SCOPE) + set(${LAUNCH_METHOD} "stand_alone" PARENT_SCOPE) else() - set(${TARGET_TYPE} "unknown" PARENT_SCOPE) + message(FATAL_ERROR "Cannot deduce test target launch method for the target ${TARGET_NAME} with type ${target_type}") endif() endfunction() -#! ly_test_impact_extract_google_test: explodes a composite google test string into namespace, test and suite components. +#! ly_test_impact_extract_google_test_name: extracts the google test name from the composite 'namespace::test_name' string # # \arg:COMPOSITE_TEST test in the form 'namespace::test' -# \arg:TEST_QUALIFER qualifier for the test (namespace) # \arg:TEST_NAME name of test -function(ly_test_impact_extract_google_test COMPOSITE_TEST TEST_QUALIFER TEST_NAME) +function(ly_test_impact_extract_google_test COMPOSITE_TEST TEST_NAMESPACE TEST_NAME) get_property(test_components GLOBAL PROPERTY LY_ALL_TESTS_${COMPOSITE_TEST}_TEST_NAME) - # Namespace and test are mandetiry + # Namespace and test are mandatory string(REPLACE "::" ";" test_components ${test_components}) list(LENGTH test_components num_test_components) if(num_test_components LESS 2) message(FATAL_ERROR "The test ${test_components} appears to have been specified without a namespace, i.e.:\ly_add_googletest/benchmark(NAME ${test_components})\nInstead of (perhaps):\ly_add_googletest/benchmark(NAME Gem::${test_components})\nPlease add the missing namespace before proceeding.") endif() - list(GET test_components 0 test_qualifier) + list(GET test_components 0 test_namespace) list(GET test_components 1 test_name) - set(${TEST_QUALIFER} ${test_qualifier} PARENT_SCOPE) + set(${TEST_NAMESPACE} ${test_namespace} PARENT_SCOPE) set(${TEST_NAME} ${test_name} PARENT_SCOPE) endfunction() -#! ly_test_impact_extract_python_test: explodes a composite python test string into filename, namespace, test and suite components. +#! ly_test_impact_extract_python_test_name: extracts the python test name from the composite 'namespace::test_name' string # # \arg:COMPOSITE_TEST test in form 'namespace::test' or 'test' -# \arg:TEST_QUALIFER qualifier for the test (optional) # \arg:TEST_NAME name of test -# \arg:TEST_FILE the Python script path for this test -function(ly_test_impact_extract_python_test COMPOSITE_TEST TEST_QUALIFER TEST_NAME TEST_FILE) +function(ly_test_impact_extract_python_test COMPOSITE_TEST TEST_NAME) get_property(test_components GLOBAL PROPERTY LY_ALL_TESTS_${COMPOSITE_TEST}_TEST_NAME) - get_property(test_file GLOBAL PROPERTY LY_ALL_TESTS_${COMPOSITE_TEST}_SCRIPT_PATH) # namespace is optional, in which case this component will be simply the test name string(REPLACE "::" ";" test_components ${test_components}) list(LENGTH test_components num_test_components) if(num_test_components GREATER 1) - list(GET test_components 0 test_qualifier) list(GET test_components 1 test_name) else() - set(test_qualifier "") set(test_name ${test_components}) endif() - # Get python script path relative to repo root - ly_test_impact_rebase_file_to_repo_root( - ${test_file} - test_file - ${LY_ROOT_FOLDER} - ) + set(${TEST_NAME} ${test_name} PARENT_SCOPE) +endfunction() - set(${TEST_QUALIFER} ${test_qualifier} PARENT_SCOPE) +#! ly_test_impact_extract_google_test_params: extracts the suites for the given google test. +# +# \arg:COMPOSITE_TEST test in the form 'namespace::test' +# \arg:COMPOSITE_SUITES composite list of suites for this target +# \arg:TEST_NAME name of test +# \arg:TEST_SUITES extracted list of suites for this target in JSON format +function(ly_test_impact_extract_google_test_params COMPOSITE_TEST COMPOSITE_SUITES TEST_NAME TEST_SUITES) + # Namespace and test are mandatory + string(REPLACE "::" ";" test_components ${COMPOSITE_TEST}) + list(LENGTH test_components num_test_components) + if(num_test_components LESS 2) + message(FATAL_ERROR "The test ${test_components} appears to have been specified without a namespace, i.e.:\ly_add_googletest/benchmark(NAME ${test_components})\nInstead of (perhaps):\ly_add_googletest/benchmark(NAME Gem::${test_components})\nPlease add the missing namespace before proceeding.") + endif() + + list(GET test_components 0 test_namespace) + list(GET test_components 1 test_name) + set(${TEST_NAMESPACE} ${test_namespace} PARENT_SCOPE) set(${TEST_NAME} ${test_name} PARENT_SCOPE) - set(${TEST_FILE} ${test_file} PARENT_SCOPE) + + set(test_suites "") + foreach(composite_suite ${COMPOSITE_SUITES}) + # Command, suite, timeout + string(REPLACE "#" ";" suite_components ${composite_suite}) + list(LENGTH suite_components num_suite_components) + if(num_suite_components LESS 3) + message(FATAL_ERROR "The suite components ${composite_suite} are required to be in the following format: command#suite#string.") + endif() + list(GET suite_components 0 test_command) + list(GET suite_components 1 test_suite) + list(GET suite_components 2 test_timeout) + set(suite_params "{ \"suite\": \"${test_suite}\", \"command\": \"${test_command}\", \"timeout\": ${test_timeout} }") + list(APPEND test_suites "${suite_params}") + endforeach() + string(REPLACE ";" ", " test_suites "${test_suites}") + set(${TEST_SUITES} ${test_suites} PARENT_SCOPE) +endfunction() + +#! ly_test_impact_extract_python_test_params: extracts the python test name and relative script path parameters. +# +# \arg:COMPOSITE_TEST test in form 'namespace::test' or 'test' +# \arg:COMPOSITE_SUITES composite list of suites for this target +# \arg:TEST_NAME name of test +# \arg:TEST_SUITES extracted list of suites for this target in JSON format +function(ly_test_impact_extract_python_test_params COMPOSITE_TEST COMPOSITE_SUITES TEST_NAME TEST_SUITES) + get_property(script_path GLOBAL PROPERTY LY_ALL_TESTS_${COMPOSITE_TEST}_SCRIPT_PATH) + + # namespace is optional, in which case this component will be simply the test name + string(REPLACE "::" ";" test_components ${COMPOSITE_TEST}) + list(LENGTH test_components num_test_components) + if(num_test_components GREATER 1) + list(GET test_components 1 test_name) + else() + set(test_name ${test_components}) + endif() + + set(${TEST_NAME} ${test_name} PARENT_SCOPE) + + set(test_suites "") + foreach(composite_suite ${COMPOSITE_SUITES}) + # Script path, suite, timeout + string(REPLACE "#" ";" suite_components ${composite_suite}) + list(LENGTH suite_components num_suite_components) + if(num_suite_components LESS 3) + message(FATAL_ERROR "The suite components ${composite_suite} are required to be in the following format: script_path#suite#string.") + endif() + list(GET suite_components 0 script_path) + list(GET suite_components 1 test_suite) + list(GET suite_components 2 test_timeout) + # Get python script path relative to repo root + ly_test_impact_rebase_file_to_repo_root( + ${script_path} + script_path + ${LY_ROOT_FOLDER} + ) + set(suite_params "{ \"suite\": \"${test_suite}\", \"script\": \"${script_path}\", \"timeout\": ${test_timeout} }") + list(APPEND test_suites "${suite_params}") + endforeach() + string(REPLACE ";" ", " test_suites "${test_suites}") + set(${TEST_SUITES} ${test_suites} PARENT_SCOPE) endfunction() #! ly_test_impact_write_test_enumeration_file: exports the master test lists to file. @@ -151,7 +217,6 @@ endfunction() # \arg:TEST_ENUMERATION_TEMPLATE_FILE path to test enumeration template file function(ly_test_impact_write_test_enumeration_file TEST_ENUMERATION_TEMPLATE_FILE) get_property(LY_ALL_TESTS GLOBAL PROPERTY LY_ALL_TESTS) - # Enumerated tests for each type set(google_tests "") set(google_benchmarks "") @@ -162,28 +227,28 @@ function(ly_test_impact_write_test_enumeration_file TEST_ENUMERATION_TEMPLATE_FI # Walk the test list foreach(test ${LY_ALL_TESTS}) message(TRACE "Parsing ${test}") + get_property(test_params GLOBAL PROPERTY LY_ALL_TESTS_${test}_PARAMS) get_property(test_type GLOBAL PROPERTY LY_ALL_TESTS_${test}_TEST_LIBRARY) - get_property(test_suite GLOBAL PROPERTY LY_ALL_TESTS_${test}_TEST_SUITE) if("${test_type}" STREQUAL "pytest") # Python tests - ly_test_impact_extract_python_test(${test} test_qualifier test_name test_file) - list(APPEND python_tests "{ name = \"${test_name}\", qualifier = \"${test_qualifier}\", suite = \"${test_suite}\", path = \"${test_file}\" }") + ly_test_impact_extract_python_test_params(${test} "${test_params}" test_name test_suites) + list(APPEND python_tests " { \"name\": \"${test_name}\", \"suites\": [${test_suites}] }") elseif("${test_type}" STREQUAL "pytest_editor") - # Python editor tests - ly_test_impact_extract_python_test(${test} test_qualifier test_name test_file) - list(APPEND python_editor_tests "{ name = \"${test_name}\", qualifier = \"${test_qualifier}\", suite = \"${test_suite}\", path = \"${test_file}\" }") + # Python editor tests + ly_test_impact_extract_python_test_params(${test} "${test_params}" test_name test_suites) + list(APPEND python_editor_tests " { \"name\": \"${test_name}\", \"suites\": [${test_suites}] }") elseif("${test_type}" STREQUAL "googletest") # Google tests - ly_test_impact_extract_google_test(${test} test_qualifier test_name) - ly_test_impact_get_target_type_string(${test_name} target_type) - list(APPEND google_tests "{ name = \"${test_name}\", qualifier = \"${test_qualifier}\", suite = \"${test_suite}\", build_type = \"${target_type}\" }") + ly_test_impact_extract_google_test_params(${test} "${test_params}" test_name test_suites) + ly_test_impact_get_test_launch_method(${test} launch_method) + list(APPEND google_tests " { \"name\": \"${test_name}\", \"launch_method\": \"${launch_method}\", \"suites\": [${test_suites}] }") elseif("${test_type}" STREQUAL "googlebenchmark") # Google benchmarks - ly_test_impact_extract_google_test(${test} test_qualifier test_name) - list(APPEND google_benchmarks "{ name = \"${test_name}\", qualifier = \"${test_qualifier}\", suite = \"${test_suite}\" }") + ly_test_impact_extract_google_test_params(${test} "${test_params}" test_name test_suites) + list(APPEND google_benchmarks " { \"name\": \"${test_name}\", \"launch_method\": \"${launch_method}\", \"suites\": [${test_suites}] }") else() - message("${test} is of unknown type (TEST_LIBRARY property is empty)") - list(APPEND unknown_tests "{ name = \"${test}\" }") + message("${test_name} is of unknown type (TEST_LIBRARY property is empty)") + list(APPEND unknown_tests " { \"name\": \"${test}\", \"type\": \"${test_type}\" }") endif() endforeach() @@ -245,12 +310,7 @@ function(ly_test_impact_export_source_target_mappings MAPPING_TEMPLATE_FILE) endif() # Static source file mappings - get_target_property(target_type ${target} TYPE) - if("${target_type}" STREQUAL "INTERFACE_LIBRARY") - get_target_property(static_sources ${target}_HEADERS SOURCES) - else() - get_target_property(static_sources ${target} SOURCES) - endif() + get_target_property(static_sources ${target} SOURCES) # Rebase static source files to repo root ly_test_impact_rebase_files_to_repo_root( @@ -274,76 +334,68 @@ endfunction() # # \arg:CONFIG_TEMPLATE_FILE path to the runtime configuration template file # \arg:PERSISTENT_DATA_DIR path to the test impact framework persistent data directory -# \arg:RUNTIME_BIN_DIR path to repo binary ourput directory -function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_DIR RUNTIME_BIN_DIR) - set(repo_dir ${LY_ROOT_FOLDER}) +# \arg:BIN_DIR path to repo binary output directory +function(ly_test_impact_write_config_file CONFIG_TEMPLATE_FILE PERSISTENT_DATA_DIR BIN_DIR) + # Platform this config file is being generated for + set(platform ${PAL_PLATFORM_NAME}) + + # Timestamp this config file was generated at + string(TIMESTAMP timestamp "%Y-%m-%d %H:%M:%S") - # SparTIA instrumentation binary + # Instrumentation binary if(NOT LY_TEST_IMPACT_INSTRUMENTATION_BIN) - message(FATAL_ERROR "No test impact framework instrumentation binary was specified, please provide the path with option LY_TEST_IMPACT_INSTRUMENTATION_BIN") + # No binary specified is not an error, it just means that the test impact analysis part of the framework is disabled + message("No test impact framework instrumentation binary was specified, test impact analysis framework will fall back to regular test sequences instead") + set(use_tiaf false) + set(instrumentation_bin "") + else() + set(use_tiaf true) + file(TO_CMAKE_PATH ${LY_TEST_IMPACT_INSTRUMENTATION_BIN} instrumentation_bin) endif() - set(instrumentation_bin ${LY_TEST_IMPACT_INSTRUMENTATION_BIN}) - - # test impact framework working dir - ly_test_impact_rebase_file_to_repo_root( - ${LY_TEST_IMPACT_WORKING_DIR} - working_dir - ${LY_ROOT_FOLDER} - ) - - # test impact framework console binary dir - ly_test_impact_rebase_file_to_repo_root( - ${RUNTIME_BIN_DIR} - runtime_bin_dir - ${LY_ROOT_FOLDER} - ) - # Test dir - ly_test_impact_rebase_file_to_repo_root( - "${PERSISTENT_DATA_DIR}/Tests" - tests_dir - ${LY_ROOT_FOLDER} - ) + # Testrunner binary + set(test_runner_bin $) + + # Repository root + set(repo_dir ${LY_ROOT_FOLDER}) - # Temp dir - ly_test_impact_rebase_file_to_repo_root( - "${LY_TEST_IMPACT_TEMP_DIR}" - temp_dir - ${LY_ROOT_FOLDER} - ) + # Test impact framework output binary dir + set(bin_dir ${BIN_DIR}) + # Temp dir + set(temp_dir "${LY_TEST_IMPACT_TEMP_DIR}") + + # Active persistent data dir + set(active_dir "${PERSISTENT_DATA_DIR}/active") + + # Historic persistent data dir + set(historic_dir "${PERSISTENT_DATA_DIR}/historic") + # Source to target mappings dir - ly_test_impact_rebase_file_to_repo_root( - "${LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR}" - source_target_mapping_dir - ${LY_ROOT_FOLDER} - ) + set(source_target_mapping_dir "${LY_TEST_IMPACT_SOURCE_TARGET_MAPPING_DIR}") - # Test type artifact dir - ly_test_impact_rebase_file_to_repo_root( - "${LY_TEST_IMPACT_TEST_TYPE_DIR}" - test_type_dir - ${LY_ROOT_FOLDER} - ) + # Test type artifact file + set(test_target_type_file "${LY_TEST_IMPACT_TEST_TYPE_FILE}") - # Bild dependency artifact dir - ly_test_impact_rebase_file_to_repo_root( - "${LY_TEST_IMPACT_TARGET_DEPENDENCY_DIR}" - target_dependency_dir - ${LY_ROOT_FOLDER} - ) + # Build dependency artifact dir + set(target_dependency_dir "${LY_TEST_IMPACT_TARGET_DEPENDENCY_DIR}") + + # Test impact analysis framework binary + set(tiaf_bin "$") # Substitute config file template with above vars file(READ "${CONFIG_TEMPLATE_FILE}" config_file) string(CONFIGURE ${config_file} config_file) # Write out entire config contents to a file in the build directory of the test impact framework console target - string(TIMESTAMP timestamp "%Y-%m-%d %H:%M:%S") - set(header "# Test Impact Framework configuration file for Lumberyard\n# Platform: ${CMAKE_SYSTEM_NAME}\n# Build: $\n# ${timestamp}") file(GENERATE - OUTPUT "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/$.$.cfg" - CONTENT "${header}\n\n${config_file}" + OUTPUT "${PERSISTENT_DATA_DIR}/$.$.json" + CONTENT ${config_file} ) + + # Set the above config file as the default config file to use for the test impact framework console target + target_compile_definitions(${LY_TEST_IMPACT_CONSOLE_STATIC_TARGET} PUBLIC "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"${PERSISTENT_DATA_DIR}/$.$.json\"") + message(DEBUG "Test impact framework post steps complete") endfunction() #! ly_test_impact_post_step: runs the post steps to be executed after all other cmake scripts have been executed. @@ -353,10 +405,9 @@ function(ly_test_impact_post_step) endif() # Directory per build config for persistent test impact data (to be checked in) - set(persistent_data_dir "${LY_ROOT_FOLDER}/Tests/test_impact_framework/${CMAKE_SYSTEM_NAME}/$") - + set(persistent_data_dir "${LY_TEST_IMPACT_WORKING_DIR}/persistent") # Directory for binaries built for this profile - set(runtime_bin_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") + set(bin_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") # Erase any existing non-persistent data to avoid getting test impact framework out of sync with current repo state file(REMOVE_RECURSE "${LY_TEST_IMPACT_WORKING_DIR}") @@ -375,14 +426,10 @@ function(ly_test_impact_post_step) ly_test_impact_write_config_file( "cmake/TestImpactFramework/ConsoleFrontendConfig.in" ${persistent_data_dir} - ${runtime_bin_dir} + ${bin_dir} ) # Copy over the graphviz options file for the build dependency graphs message(DEBUG "Test impact framework config file written") file(COPY "cmake/TestImpactFramework/CMakeGraphVizOptions.cmake" DESTINATION ${CMAKE_BINARY_DIR}) - - # Set the above config file as the default config file to use for the test impact framework console target - target_compile_definitions(${LY_TEST_IMPACT_CONSOLE_TARGET} PRIVATE "LY_TEST_IMPACT_DEFAULT_CONFIG_FILE=\"$.$.cfg\"") - message(DEBUG "Test impact framework post steps complete") -endfunction() \ No newline at end of file +endfunction() diff --git a/cmake/TestImpactFramework/SourceToTargetMapping.in b/cmake/TestImpactFramework/SourceToTargetMapping.in index c94c52a145..da837ed398 100644 --- a/cmake/TestImpactFramework/SourceToTargetMapping.in +++ b/cmake/TestImpactFramework/SourceToTargetMapping.in @@ -1,15 +1,18 @@ -#Lumberyard source to target mapping -[target] -name = "${target_name}" -output_name = "${target_output_name}" -path = "${target_path}" -[sources] -input = [ +{ + "sources": { + "input": [ ${autogen_input_files} -] -output = [ + ], + "output": [ ${autogen_output_files} -] -static = [ + ], + "static": [ ${static_sources} -] \ No newline at end of file + ] + }, + "target": { + "name": "${target_name}", + "output_name": "${target_output_name}", + "path": "${target_path}" + } +} \ No newline at end of file diff --git a/cmake/Tools/Platform/Android/android_deployment.py b/cmake/Tools/Platform/Android/android_deployment.py index eb8361ec18..82d14a2746 100755 --- a/cmake/Tools/Platform/Android/android_deployment.py +++ b/cmake/Tools/Platform/Android/android_deployment.py @@ -184,11 +184,15 @@ class AndroidDeployment(object): call_arguments.extend(arg_list) - output = subprocess.check_output(call_arguments, - shell=True, - stderr=subprocess.DEVNULL).decode(common.DEFAULT_TEXT_READ_ENCODING, - common.ENCODING_ERROR_HANDLINGS) - return output + try: + output = subprocess.check_output(call_arguments, + shell=True, + stderr=subprocess.PIPE).decode(common.DEFAULT_TEXT_READ_ENCODING, + common.ENCODING_ERROR_HANDLINGS) + return output + except subprocess.CalledProcessError as err: + raise common.LmbrCmdError(err.stderr.decode(common.DEFAULT_TEXT_READ_ENCODING, + common.ENCODING_ERROR_HANDLINGS)) def adb_shell(self, command, device_id): """ diff --git a/cmake/Tools/Platform/Android/generate_android_project.py b/cmake/Tools/Platform/Android/generate_android_project.py index d25b62dde8..5a52ac385e 100755 --- a/cmake/Tools/Platform/Android/generate_android_project.py +++ b/cmake/Tools/Platform/Android/generate_android_project.py @@ -48,7 +48,7 @@ def verify_gradle(override_gradle_path=None): CMAKE_ARGUMENT_NAME = '--cmake-install-path' -CMAKE_MIN_VERSION = LooseVersion('3.19.0') +CMAKE_MIN_VERSION = LooseVersion('3.20.0') CMAKE_VERSION_REGEX = re.compile(r'cmake version (\d+.\d+.?\d*)') CMAKE_EXECUTABLE = 'cmake' diff --git a/cmake/Tools/common.py b/cmake/Tools/common.py index 1990041c86..cf4631c227 100755 --- a/cmake/Tools/common.py +++ b/cmake/Tools/common.py @@ -274,6 +274,8 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too :return: Tuple of the resolved tool version and the resolved override tool path if provided """ + tool_source = tool_name + try: # Use either the provided gradle override or the gradle in the path environment if override_tool_path: @@ -306,18 +308,17 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too tool_desc = f"{tool_name} path provided in the command line argument '{argument_name}={override_tool_path}' " else: resolved_override_tool_path = None - tool_source = tool_name tool_desc = f"installed {tool_name} in the system path" # Extract the version and verify version_output = subprocess.check_output([tool_source, tool_version_argument], - shell=True).decode(DEFAULT_TEXT_READ_ENCODING, - ENCODING_ERROR_HANDLINGS) + shell=True, + stderr=subprocess.PIPE).decode(DEFAULT_TEXT_READ_ENCODING, + ENCODING_ERROR_HANDLINGS) version_match = tool_version_regex.search(version_output) if not version_match: raise RuntimeError() - # Since we are doing a compare, strip out any non-numeric and non . character from the version otherwise we will get a TypeError on the LooseVersion comparison result_version_str = re.sub(r"[^\.0-9]", "", str(version_match.group(1)).strip()) result_version = LooseVersion(result_version_str) @@ -331,7 +332,14 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too return result_version, resolved_override_tool_path - except (CalledProcessError, WindowsError, RuntimeError) as e: + except CalledProcessError as e: + error_msg = e.output.decode(DEFAULT_TEXT_READ_ENCODING, + ENCODING_ERROR_HANDLINGS) + raise LmbrCmdError(f"{tool_name} cannot be resolved or there was a problem determining its version number. " + f"Either make sure its in the system path environment or a valid path is passed in " + f"through the {argument_name} argument.\n{error_msg}", + ERROR_CODE_ERROR_NOT_SUPPORTED) + except (WindowsError, RuntimeError) as e: logging.error(f"Call to '{tool_source}' resulted in error: {e}") raise LmbrCmdError(f"{tool_name} cannot be resolved or there was a problem determining its version number. " f"Either make sure its in the system path environment or a valid path is passed in " diff --git a/cmake/cmake_files.cmake b/cmake/cmake_files.cmake index a1fd66a06d..3d7ca9794f 100644 --- a/cmake/cmake_files.cmake +++ b/cmake/cmake_files.cmake @@ -35,7 +35,6 @@ set(FILES PAL.cmake PALTools.cmake Projects.cmake - run_epbtest.cmake RuntimeDependencies.cmake SettingsRegistry.cmake UnitTest.cmake diff --git a/cmake/run_epbtest.cmake b/cmake/run_epbtest.cmake deleted file mode 100644 index a0c5eb2b17..0000000000 --- a/cmake/run_epbtest.cmake +++ /dev/null @@ -1,42 +0,0 @@ -# -# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -# its licensors. -# -# For complete copyright and license terms please see the LICENSE at the root of this -# distribution (the "License"). All use of this software is governed by the License, -# or, if provided, by the license below or the license accompanying this file. Do not -# remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# - -# Script for running a test that uses EditorPythonBindings. Takes care of: -# 1. Activating a project. -# 2. Enabling the EditorPythonBindings gem. -# 3. Invoking the Editor executable with the parameters to load the test script. -# 4. Kills the AssetProcessor process -# The following arguments are required: -# CMD_ARG_TEST_PROJECT - name of the project to enable via lmbr. -# CMD_ARG_EDITOR - full path to the Editor executable. -# CMD_ARG_PYTHON_SCRIPT - full path to the python script to be executed by the Editor. - -# EditorPythonBindings need to be enabled for the project we launch - -execute_process( - COMMAND ${CMD_ARG_EDITOR} -NullRenderer --skipWelcomeScreenDialog --autotest_mode --regset="/Amazon/AzCore/Bootstrap/project_path=${CMD_ARG_TEST_PROJECT}" --runpython ${CMD_ARG_PYTHON_SCRIPT} - TIMEOUT 1800 - RESULT_VARIABLE TEST_CMD_RESULT -) - -if(${PLATFORM} STREQUAL "Windows") - execute_process( - COMMAND taskkill /F /IM AssetProcessor.exe - ) -else() - execute_process( - COMMAND killall -I AssetProcessor - ) -endif() - -if(TEST_CMD_RESULT) - message(FATAL_ERROR "Error running EditorPythonBindings Test via CMake Wrapper, result ${TEST_CMD_RESULT}") -endif() \ No newline at end of file diff --git a/python/get_python.bat b/python/get_python.bat index e11c4ab92f..b3c3bf9cfb 100644 --- a/python/get_python.bat +++ b/python/get_python.bat @@ -32,18 +32,15 @@ IF !ERRORLEVEL!==0 ( cd /D %CMD_DIR%\.. REM IF you update this logic, update it in scripts/build/Platform/Windows/env_windows.cmd -REM If cmake is not found on path, try a known location, using LY_CMAKE_PATH as the first fallback +REM If cmake is not found on path, try a known location at LY_CMAKE_PATH where /Q cmake IF NOT !ERRORLEVEL!==0 ( IF "%LY_CMAKE_PATH%"=="" ( - IF "%LY_3RDPARTY_PATH%"=="" ( - ECHO ERROR: CMake was not found on the PATH and LY_3RDPARTY_PATH is not defined. - ECHO Please ensure CMake is on the path or set LY_3RDPARTY_PATH or LY_CMAKE_PATH. - EXIT /b 1 - ) - SET LY_CMAKE_PATH=!LY_3RDPARTY_PATH!\CMake\3.19.1\Windows\bin - echo CMake was not found on the path, will use known location: !LY_CMAKE_PATH! + ECHO ERROR: CMake was not found on the PATH and LY_CMAKE_PATH is not defined. + ECHO Please ensure CMake is on the path or set LY_CMAKE_PATH. + EXIT /b 1 ) + PATH !LY_CMAKE_PATH!;!PATH! where /Q cmake if NOT !ERRORLEVEL!==0 ( diff --git a/python/get_python.cmake b/python/get_python.cmake index 5d6bf6fd96..198d3f0ba3 100644 --- a/python/get_python.cmake +++ b/python/get_python.cmake @@ -16,7 +16,7 @@ # example: # cmake -DPAL_PLATFORM_NAME:string=Windows -DLY_3RDPARTY_PATH:string=%CMD_DIR% -P get_python.cmake -cmake_minimum_required(VERSION 3.17) +cmake_minimum_required(VERSION 3.20) if(LY_3RDPARTY_PATH) file(TO_CMAKE_PATH ${LY_3RDPARTY_PATH} LY_3RDPARTY_PATH) diff --git a/python/get_python.sh b/python/get_python.sh index 780f1bbdd8..9b774d91fc 100755 --- a/python/get_python.sh +++ b/python/get_python.sh @@ -42,31 +42,21 @@ then CMAKE_FOLDER_RELATIVE_TO_ROOT=CMake.app/Contents/bin else PAL=Linux - CMAKE_FOLDER_RELATIVE_TO_ROOT=bin + CMAKE_FOLDER_RELATIVE_TO_ROOT=bin fi if ! [ -x "$(command -v cmake)" ]; then - # Note that LY_3RDPARTY_PATH is only required here if you have no cmake in your PATH. if [ -z ${LY_CMAKE_PATH} ]; then - if [ -z ${LY_3RDPARTY_PATH} ]; then - echo "ERROR: Could not find cmake on the PATH and LY_3RDPARTY_PATH is not defined, cannot continue." - echo "Please add cmake to your PATH, or define $LY_3RDPARTY_PATH" - exit 1 - fi - LY_CMAKE_PATH=$LY_3RDPARTY_PATH/CMake/3.19.1/$PAL/$CMAKE_FOLDER_RELATIVE_TO_ROOT - # if you change the version number, change it also in: - # scripts/build/Platform/Mac/env_mac.sh - # and - # scripts/build/Platform/Linux/env_linux.sh + echo "ERROR: Could not find cmake on the PATH and LY_CMAKE_PATH is not defined, cannot continue." + echo "Please add cmake to your PATH, or define LY_CMAKE_PATH" + exit 1 fi - + export PATH=$LY_CMAKE_PATH:$PATH if ! [ -x "$(command -v cmake)" ]; then - echo "ERROR: Could not find cmake on the PATH or at the known location: $CMAKE_KNOWN_LOCATION" + echo "ERROR: Could not find cmake on the PATH or at the known location: $LY_CMAKE_PATH" echo "Please add cmake to the environment PATH or place it at the above known location." exit 1 - else - echo "CMake not found on path, but was found in the known 3rd Party location." fi fi diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index 693cf31727..c52f432bed 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -450,7 +450,7 @@ try { // repositoryName is the full repository name repositoryName = (repositoryUrl =~ /https:\/\/github.com\/(.*)\.git/)[0][1] (projectName, pipelineName) = GetRunningPipelineName(env.JOB_NAME) // env.JOB_NAME is the name of the job given by Jenkins - + env.PIPELINE_NAME = pipelineName if(env.BRANCH_NAME) { branchName = env.BRANCH_NAME } else { diff --git a/scripts/build/Platform/Android/build_and_run_unit_tests.cmd b/scripts/build/Platform/Android/build_and_run_unit_tests.cmd index fbb31f95ec..5a51621f40 100644 --- a/scripts/build/Platform/Android/build_and_run_unit_tests.cmd +++ b/scripts/build/Platform/Android/build_and_run_unit_tests.cmd @@ -20,9 +20,6 @@ IF NOT EXIST "%LY_3RDPARTY_PATH%" ( GOTO :error ) -IF NOT EXIST "%LY_ANDROID_SDK%" ( - SET LY_ANDROID_SDK=!LY_3RDPARTY_PATH!/android-sdk/platform-29 -) IF NOT EXIST "%LY_ANDROID_SDK%" ( ECHO [ci_build] FAIL: LY_ANDROID_SDK=!LY_ANDROID_SDK! GOTO :error diff --git a/scripts/build/Platform/Linux/build_config.json b/scripts/build/Platform/Linux/build_config.json index 660bc50da6..59b72ec4b0 100644 --- a/scripts/build/Platform/Linux/build_config.json +++ b/scripts/build/Platform/Linux/build_config.json @@ -83,7 +83,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", - "CTEST_OPTIONS": "-E (Gem::EMotionFX.Editor.Tests|Gem::AWSClientAuth.Tests|Gem::AWSCore.Editor.Tests) -LE SUITE_sandbox -L FRAMEWORK_googletest" + "CTEST_OPTIONS": "-E (Gem::EMotionFX.Editor.Tests|Gem::AWSCore.Editor.Tests) -LE SUITE_sandbox -L FRAMEWORK_googletest" } }, "test_profile_nounity": { @@ -95,7 +95,7 @@ "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=FALSE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", - "CTEST_OPTIONS": "-E (Gem::EMotionFX.Editor.Tests|Gem::AWSClientAuth.Tests|Gem::AWSCore.Editor.Tests) -LE SUITE_sandbox -L FRAMEWORK_googletest" + "CTEST_OPTIONS": "-E (Gem::EMotionFX.Editor.Tests|Gem::AWSCore.Editor.Tests) -LE SUITE_sandbox -L FRAMEWORK_googletest" } }, "asset_profile": { diff --git a/scripts/build/Platform/Linux/env_linux.sh b/scripts/build/Platform/Linux/env_linux.sh index 1d7324778c..85bdf0a745 100755 --- a/scripts/build/Platform/Linux/env_linux.sh +++ b/scripts/build/Platform/Linux/env_linux.sh @@ -13,27 +13,11 @@ set -o errexit # exit on the first failure encountered if ! command -v cmake &> /dev/null; then - if [[ -z $LY_CMAKE_PATH ]]; then LY_CMAKE_PATH=${LY_3RDPARTY_PATH}/CMake/3.19.1/Linux/bin; fi - if [[ ! -d $LY_CMAKE_PATH ]]; then - echo "[ci_build] CMake path not found" - exit 1 - fi - PATH=${LY_CMAKE_PATH}:${PATH} - if ! command -v cmake &> /dev/null; then - echo "[ci_build] CMake not found" - exit 1 - fi + echo "[ci_build] CMake not found" + exit 1 fi if ! command -v ninja &> /dev/null; then - if [[ -z $LY_NINJA_PATH ]]; then LY_NINJA_PATH=${LY_3RDPARTY_PATH}/ninja/1.10.1/Linux; fi - if [[ ! -d $LY_NINJA_PATH ]]; then - echo "[ci_build] Ninja path not found" - exit 1 - fi - PATH=${LY_NINJA_PATH}:${PATH} - if ! command -v ninja &> /dev/null; then - echo "[ci_build] Ninja not found" - exit 1 - fi + echo "[ci_build] Ninja not found" + exit 1 fi diff --git a/scripts/build/Platform/Mac/env_mac.sh b/scripts/build/Platform/Mac/env_mac.sh index 8f38d1d90b..917329b6af 100755 --- a/scripts/build/Platform/Mac/env_mac.sh +++ b/scripts/build/Platform/Mac/env_mac.sh @@ -13,14 +13,6 @@ set -o errexit # exit on the first failure encountered if ! command -v cmake &> /dev/null; then - if [[ -z $LY_CMAKE_PATH ]]; then LY_CMAKE_PATH=${LY_3RDPARTY_PATH}/CMake/3.19.1/Mac/CMake.app/Contents/bin; fi - if [[ ! -d $LY_CMAKE_PATH ]]; then - echo "[ci_build] CMake path not found" - exit 1 - fi - PATH=${LY_CMAKE_PATH}:${PATH} - if ! command -v cmake &> /dev/null; then - echo "[ci_build] CMake not found" - exit 1 - fi + echo "[ci_build] CMake not found" + exit 1 fi diff --git a/scripts/build/Platform/Windows/build_config.json b/scripts/build/Platform/Windows/build_config.json index 71abf9021f..e37e467fa0 100644 --- a/scripts/build/Platform/Windows/build_config.json +++ b/scripts/build/Platform/Windows/build_config.json @@ -27,10 +27,13 @@ }, "profile_vs2019_pipe": { "TAGS": [ - "default" + "default", + "nightly-incremental", + "nightly-clean" ], "steps": [ "profile_vs2019", + "test_impact_analysis", "asset_profile_vs2019", "test_cpu_profile_vs2019" ] @@ -79,6 +82,15 @@ "SCRIPT_PARAMETERS": "--platform 3rdParty --type 3rdParty_all" } }, + "test_impact_analysis": { + "TAGS": [ + ], + "COMMAND": "python_windows.cmd", + "PARAMETERS": { + "SCRIPT_PATH": "scripts/build/TestImpactAnalysis/tiaf_driver.py", + "SCRIPT_PARAMETERS": "--testFailurePolicy=continue --suite main --pipeline !PIPELINE_NAME! --destCommit !CHANGE_ID! --config \"build\\windows_vs2019\\bin\\TestImpactFramework\\persistent\\tiaf.profile.json\"" + } + }, "debug_vs2019": { "TAGS": [ "weekly-build-metrics" @@ -119,7 +131,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DLY_TEST_IMPACT_ACTIVE=1 -DLY_TEST_IMPACT_INSTRUMENTATION_BIN=!TEST_IMPACT_WIN_BINARY!", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -177,7 +189,8 @@ "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", "CTEST_OPTIONS": "-L \"(SUITE_smoke_REQUIRES_gpu|SUITE_main_REQUIRES_gpu)\" -T Test", "TEST_METRICS": "True", - "TEST_RESULTS": "True" + "TEST_RESULTS": "True", + "TEST_SCREENSHOTS": "True" } }, "asset_profile_vs2019": { diff --git a/scripts/build/Platform/Windows/build_ninja_windows.cmd b/scripts/build/Platform/Windows/build_ninja_windows.cmd index b782cc2357..e1a802e254 100644 --- a/scripts/build/Platform/Windows/build_ninja_windows.cmd +++ b/scripts/build/Platform/Windows/build_ninja_windows.cmd @@ -12,19 +12,10 @@ REM SETLOCAL EnableDelayedExpansion -IF NOT EXIST "%LY_NINJA_PATH%" ( - SET LY_NINJA_PATH=%LY_3RDPARTY_PATH%/ninja/1.10.1/Windows -) -IF NOT EXIST "%LY_NINJA_PATH%" ( - ECHO [ci_build] FAIL: LY_NINJA_PATH=%LY_NINJA_PATH% - GOTO :error -) -PATH %LY_NINJA_PATH%;%PATH% - CALL "%~dp0build_windows.cmd" IF NOT %ERRORLEVEL%==0 GOTO :error EXIT /b 0 :error -EXIT /b 1 \ No newline at end of file +EXIT /b 1 diff --git a/scripts/build/Platform/Windows/env_windows.cmd b/scripts/build/Platform/Windows/env_windows.cmd index 592b2867cd..3ef161c2ef 100644 --- a/scripts/build/Platform/Windows/env_windows.cmd +++ b/scripts/build/Platform/Windows/env_windows.cmd @@ -12,17 +12,8 @@ REM where /Q cmake IF NOT %ERRORLEVEL%==0 ( - IF "%LY_CMAKE_PATH%"=="" (SET LY_CMAKE_PATH=%LY_3RDPARTY_PATH%/CMake/3.19.1/Windows/bin) - IF NOT EXIST !LY_CMAKE_PATH! ( - ECHO [ci_build] CMake path not found - GOTO :error - ) - PATH !LY_CMAKE_PATH!;!PATH! - where /Q cmake - IF NOT !ERRORLEVEL!==0 ( - ECHO [ci_build] CMake not found - GOTO :error - ) + ECHO [ci_build] CMake not found + GOTO :error ) EXIT /b 0 diff --git a/scripts/build/TestImpactAnalysis/git_utils.py b/scripts/build/TestImpactAnalysis/git_utils.py new file mode 100644 index 0000000000..5abedd16f0 --- /dev/null +++ b/scripts/build/TestImpactAnalysis/git_utils.py @@ -0,0 +1,42 @@ +# +# 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 subprocess +import git + +# Returns True if the dst commit descends from the src commit, otherwise False +def is_descendent(src_commit_hash, dst_commit_hash): + if src_commit_hash is None or dst_commit_hash is None: + return False + result = subprocess.run(["git", "merge-base", "--is-ancestor", src_commit_hash, dst_commit_hash]) + return result.returncode == 0 + +# Attempts to create a diff from the src and dst commits and write to the specified output file +def create_diff_file(src_commit_hash, dst_commit_hash, output_path): + if os.path.isfile(output_path): + os.remove(output_path) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + # git diff will only write to the output file if both commit hashes are valid + subprocess.run(["git", "diff", "--name-status", f"--output={output_path}", src_commit_hash, dst_commit_hash]) + if not os.path.isfile(output_path): + raise FileNotFoundError(f"Source commit '{src_commit_hash}' and/or destination commit '{dst_commit_hash}' are invalid") + +# Basic representation of a repository +class Repo: + def __init__(self, repo_path): + self.__repo = git.Repo(repo_path) + + # Returns the current branch + @property + def current_branch(self): + branch = self.__repo.active_branch + return branch.name diff --git a/scripts/build/TestImpactAnalysis/tiaf.py b/scripts/build/TestImpactAnalysis/tiaf.py new file mode 100644 index 0000000000..19c1b2754d --- /dev/null +++ b/scripts/build/TestImpactAnalysis/tiaf.py @@ -0,0 +1,222 @@ +# +# 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 json +import subprocess +import re +import git_utils +from git_utils import Repo +from enum import Enum + +# Returns True if the specified child path is a child of the specified parent path, otherwise False +def is_child_path(parent_path, child_path): + parent_path = os.path.abspath(parent_path) + child_path = os.path.abspath(child_path) + return os.path.commonpath([os.path.abspath(parent_path)]) == os.path.commonpath([os.path.abspath(parent_path), os.path.abspath(child_path)]) + +class TestImpact: + def __init__(self, config_file, pipeline, dst_commit): + self.__pipeline = pipeline + self.__dst_commit = dst_commit + self.__src_commit = None + self.__has_src_commit = False + self.__parse_config_file(config_file) + if self.__use_test_impact_analysis and not self.__is_pipeline_of_truth: + self.__generate_change_list() + + # Parse the configuration file and retrieve the data needed for launching the test impact analysis runtime + def __parse_config_file(self, config_file): + print(f"Attempting to parse configuration file '{config_file}'...") + with open(config_file, "r") as config_data: + config = json.load(config_data) + # Repository + self.__repo_dir = config["repo"]["root"] + # Jenkins + self.__use_test_impact_analysis = config["jenkins"]["use_test_impact_analysis"] + self.__pipeline_of_truth = config["jenkins"]["pipeline_of_truth"] + print(f"Pipeline of truth: '{self.__pipeline_of_truth}'.") + print(f"This pipeline: '{self.__pipeline}'.") + if self.__pipeline in self.__pipeline_of_truth: + self.__is_pipeline_of_truth = True + else: + self.__is_pipeline_of_truth = False + print(f"Is pipeline of truth: '{self.__is_pipeline_of_truth}'.") + # TIAF binary + self.__tiaf_bin = config["repo"]["tiaf_bin"] + if self.__use_test_impact_analysis and not os.path.isfile(self.__tiaf_bin): + raise FileNotFoundError("Could not find tiaf binary") + # Workspaces + self.__active_workspace = config["workspace"]["active"]["root"] + self.__historic_workspace = config["workspace"]["historic"]["root"] + self.__temp_workspace = config["workspace"]["temp"]["root"] + # Last commit hash + last_commit_hash_path_file = config["workspace"]["historic"]["relative_paths"]["last_run_hash_file"] + self.__last_commit_hash_path = os.path.join(self.__historic_workspace, last_commit_hash_path_file) + print("The configuration file was parsed successfully.") + + # Restricts change lists from checking in test impact analysis files + def __check_for_restricted_files(self, file_path): + if is_child_path(self.__active_workspace, file_path) or is_child_path(self.__historic_workspace, file_path) or is_child_path(self.__temp_workspace, file_path): + raise ValueError(f"Checking in test impact analysis framework files is illegal: '{file_path}''.") + + def __read_last_run_hash(self): + self.__has_src_commit = False + if os.path.isfile(self.__last_commit_hash_path): + print(f"Previous commit hash found at '{self.__last_commit_hash_path}'.") + with open(self.__last_commit_hash_path) as file: + self.__src_commit = file.read() + self.__has_src_commit = True + + def __write_last_run_hash(self, last_run_hash): + os.mkdir(self.__historic_workspace) + f = open(self.__last_commit_hash_path, "w") + f.write(last_run_hash) + f.close() + + # Determines the change list bewteen now and the last tiaf run (if any) + def __generate_change_list(self): + self.__has_change_list = False + self.__change_list_path = None + # Check whether or not a previous commit hash exists (no hash is not a failure) + self.__read_last_run_hash() + if self.__has_src_commit == True: + if git_utils.is_descendent(self.__src_commit, self.__dst_commit) == False: + print(f"Source commit '{self.__src_commit}' and destination commit '{self.__dst_commit}' are not related.") + return + diff_path = os.path.join(self.__temp_workspace, "changelist.diff") + try: + git_utils.create_diff_file(self.__src_commit, self.__dst_commit, diff_path) + except FileNotFoundError as e: + print(e) + return + # A diff was generated, attempt to parse the diff and construct the change list + print(f"Generated diff between commits '{self.__src_commit}' and '{self.__dst_commit}': '{diff_path}'.") + change_list = {} + change_list["createdFiles"] = [] + change_list["updatedFiles"] = [] + change_list["deletedFiles"] = [] + with open(diff_path, "r") as diff_data: + lines = diff_data.readlines() + for line in lines: + match = re.split("^R[0-9]+\\s(\\S+)\\s(\\S+)", line) + if len(match) > 1: + # File rename + self.__check_for_restricted_files(match[1]) + self.__check_for_restricted_files(match[2]) + # Treat renames as a deletion and an addition + change_list["deletedFiles"].append(match[1]) + change_list["createdFiles"].append(match[2]) + else: + match = re.split("^[AMD]\\s(\\S+)", line) + self.__check_for_restricted_files(match[1]) + if len(match) > 1: + if line[0] == 'A': + # File addition + change_list["createdFiles"].append(match[1]) + elif line[0] == 'M': + # File modification + change_list["updatedFiles"].append(match[1]) + elif line[0] == 'D': + # File Deletion + change_list["deletedFiles"].append(match[1]) + # Serialize the change list to the JSON format the test impact analysis runtime expects + change_list_json = json.dumps(change_list, indent = 4) + change_list_path = os.path.join(self.__temp_workspace, "changelist.json") + f = open(change_list_path, "w") + f.write(change_list_json) + f.close() + print(f"Change list constructed successfully: '{change_list_path}'.") + print(f"{len(change_list['createdFiles'])} created files, {len(change_list['updatedFiles'])} updated files and {len(change_list['deletedFiles'])} deleted files.") + # Note: an empty change list generated due to no changes between last and current commit is valid + self.__has_change_list = True + self.__change_list_path = change_list_path + else: + print("No previous commit hash found, regular or seeded sequences only will be run.") + self.__has_change_list = False + return + + # Runs the specified test sequence + def run(self, suite, test_failure_policy, safe_mode, test_timeout, global_timeout): + args = [] + pipeline_of_truth_test_failure_policy = "continue" + # Suite + args.append(f"--suite={suite}") + print(f"Test suite is set to '{suite}'.") + # Timeouts + if test_timeout != None: + args.append(f"--ttimeout={test_timeout}") + print(f"Test target timeout is set to {test_timeout} seconds.") + if global_timeout != None: + args.append(f"--gtimeout={global_timeout}") + print(f"Global sequence timeout is set to {test_timeout} seconds.") + if self.__use_test_impact_analysis: + print("Test impact analysis is enabled.") + # Pipeline of truth sequence + if self.__is_pipeline_of_truth: + # Sequence type + args.append("--sequence=seed") + print("Sequence type is set to 'seed'.") + # Test failure policy + args.append(f"--fpolicy={pipeline_of_truth_test_failure_policy}") + print(f"Test failure policy is set to '{pipeline_of_truth_test_failure_policy}'.") + # Non pipeline of truth sequence + else: + if self.__has_change_list: + # Change list + args.append(f"--changelist={self.__change_list_path}") + print(f"Change list is set to '{self.__change_list_path}'.") + # Sequence type + args.append("--sequence=tianowrite") + print("Sequence type is set to 'tianowrite'.") + # Safe mode + if safe_mode: + args.append("--safemode=on") + print("Safe mode set to 'on'.") + else: + args.append("--safemode=off") + print("Safe mode set to 'off'.") + else: + args.append("--sequence=regular") + print("Sequence type is set to 'regular'.") + # Test failure policy + args.append(f"--fpolicy={test_failure_policy}") + print(f"Test failure policy is set to '{test_failure_policy}'.") + else: + print("Test impact analysis ie disabled.") + # Sequence type + args.append("--sequence=regular") + print("Sequence type is set to 'seed'.") + # Pipeline of truth sequence + if self.__is_pipeline_of_truth: + # Test failure policy + args.append(f"--fpolicy={pipeline_of_truth_test_failure_policy}") + print(f"Test failure policy is set to '{pipeline_of_truth_test_failure_policy}'.") + # Non pipeline of truth sequence + else: + # Test failure policy + args.append(f"--fpolicy={test_failure_policy}") + print(f"Test failure policy is set to '{test_failure_policy}'.") + + print("Args: ", end='') + print(*args) + result = subprocess.run([self.__tiaf_bin] + args) + # If the sequence completed 9with or without failures) we will update the historical meta-data + if result.returncode == 0 or result.returncode == 7: + print("Test impact analysis runtime returned successfully.") + if self.__is_pipeline_of_truth: + print("Writing historical meta-data...") + self.__write_last_run_hash(self.__dst_commit) + print("Complete!") + else: + print(f"The test impact analysis runtime returned with error: '{result.returncode}'.") + return result.returncode + \ No newline at end of file diff --git a/scripts/build/TestImpactAnalysis/tiaf_driver.py b/scripts/build/TestImpactAnalysis/tiaf_driver.py new file mode 100644 index 0000000000..a21f502a2a --- /dev/null +++ b/scripts/build/TestImpactAnalysis/tiaf_driver.py @@ -0,0 +1,66 @@ +# +# 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 argparse +from tiaf import TestImpact + +import sys +import os +import datetime +import json +import socket + +def parse_args(): + def file_path(value): + if os.path.isfile(value): + return value + else: + raise FileNotFoundError(value) + + def timout_type(value): + value = int(value) + if value <= 0: + raise ValueError("Timer values must be positive integers") + return value + + def test_failure_policy(value): + if value == "continue" or value == "abort" or value == "ignore": + return value + else: + raise ValueError("Test failure policy must be 'abort', 'continue' or 'ignore'") + + parser = argparse.ArgumentParser() + parser.add_argument('--config', dest="config", type=file_path, help="Path to the test impact analysis framework configuration file", required=True) + parser.add_argument('--pipeline', dest="pipeline", help="Pipeline the test impact analysis framework is running on", required=True) + parser.add_argument('--destCommit', dest="dst_commit", help="Commit to run test impact analysis on (ignored when seeding)", required=True) + parser.add_argument('--suite', dest="suite", help="Test suite to run", required=True) + parser.add_argument('--testFailurePolicy', dest="test_failure_policy", type=test_failure_policy, help="Test failure policy for regular and test impact sequences (ignored when seeding)", required=True) + parser.add_argument('--safeMode', dest="safe_mode", action='store_true', help="Run impact analysis tests in safe mode (ignored when seeding)") + parser.add_argument('--testTimeout', dest="test_timeout", type=timout_type, help="Maximum run time (in seconds) of any test target before being terminated", required=False) + parser.add_argument('--globalTimeout', dest="global_timeout", type=timout_type, help="Maximum run time of the sequence before being terminated", required=False) + parser.set_defaults(test_failure_policy="abort") + parser.set_defaults(test_timeout=None) + parser.set_defaults(global_timeout=None) + args = parser.parse_args() + + return args + +if __name__ == "__main__": + try: + args = parse_args() + tiaf = TestImpact(args.config, args.pipeline, args.dst_commit) + return_code = tiaf.run(args.suite, args.test_failure_policy, args.safe_mode, args.test_timeout, args.global_timeout) + # Non-gating will be removed from this script and handled at the job level in SPEC-7413 + #sys.exit(return_code) + sys.exit(0) + except: + # Non-gating will be removed from this script and handled at the job level in SPEC-7413 + sys.exit(0) \ No newline at end of file diff --git a/scripts/build/tools/upload_to_s3.py b/scripts/build/tools/upload_to_s3.py index 5dfe5eb66e..5666e6ec6b 100755 --- a/scripts/build/tools/upload_to_s3.py +++ b/scripts/build/tools/upload_to_s3.py @@ -17,12 +17,16 @@ python upload_to_s3.py --base_dir %WORKSPACE% --file_regex "(.*zip$|.*MD5$)" --b Use profile to upload all .zip and .MD5 files in %WORKSPACE% folder to bucket ly-packages-mainline: python upload_to_s3.py --base_dir %WORKSPACE% --profile profile --file_regex "(.*zip$|.*MD5$)" --bucket ly-packages-mainline +Another example usage for uploading all .png and .ppm files inside base_dir and only subdirectories within base_dir: +python upload_to_s3.py --base_dir %WORKSPACE%/path/to/files --file_regex "(.*png$|.*ppm$)" --bucket screenshot-test-bucket --search_subdirectories True --key_prefix Test + ''' import os import re import json +import time import boto3 from optparse import OptionParser @@ -34,6 +38,8 @@ def parse_args(): parser.add_option("--profile", dest="profile", default=None, help="The name of a profile to use. If not given, then the default profile is used.") parser.add_option("--bucket", dest="bucket", default=None, help="S3 bucket the files are uploaded to.") parser.add_option("--key_prefix", dest="key_prefix", default='', help="Object key prefix.") + parser.add_option("--search_subdirectories", dest="search_subdirectories", action='store_true', + help="Toggle for searching for files in subdirectories beneath base_dir, defaults to False") ''' ExtraArgs used to call s3.upload_file(), should be in json format. extra_args key must be one of: ACL, CacheControl, ContentDisposition, ContentEncoding, ContentLanguage, ContentType, Expires, GrantFullControl, GrantRead, GrantReadACP, GrantWriteACP, Metadata, RequestPayer, ServerSideEncryption, StorageClass, @@ -62,48 +68,82 @@ def get_client(service_name, profile_name): return client -def get_files_to_upload(base_dir, regex): +def get_files_to_upload(base_dir, regex, search_subdirectories): + """ + Uses a regex expression pattern to return a list of file paths for files to upload to the s3 bucket. + :param base_dir: path for the base directory, if using search_subdirectories=True ensure this is the parent. + :param regex: pattern to use for regex searching, ex. "(.*zip$|.*MD5$)" + :param search_subdirectories: boolean False for only getting files in base_dir, True to get all files in base_dir + and any subdirectory inside base_dir, defaults to False from the parse_args() function. + :return: a list of string file paths for files to upload to the s3 bucket matching the regex expression. + """ # Get all file names in base directory - files = [x for x in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, x))] - # strip the surround quotes, if they exist + files = [os.path.join(base_dir, x) for x in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, x))] + if search_subdirectories: # Get all file names in base directory and any subdirectories. + for subdirectory in os.walk(base_dir): + # Example output for subdirectory: + # ('C:\path\to\base_dir\', ['Subfolder1', 'Subfolder2'], ['file1', 'file2']) + subdirectory_file_path = subdirectory[0] + subdirectory_files = subdirectory[2] + if subdirectory_files: + subdirectory_file_paths = _build_file_paths(subdirectory_file_path, subdirectory_files) + files.extend(subdirectory_file_paths) + try: - regex = json.loads(regex) + regex = json.loads(regex) # strip the surround quotes, if they exist except: + print(f'WARNING: failed to call json.loads() for regex: "{regex}"') pass # Get all file names matching the regular expression, those file will be uploaded to S3 - files_to_upload = [x for x in files if re.match(regex, x)] - return files_to_upload + regex_files_to_upload = [x for x in files if re.match(regex, x)] + + return regex_files_to_upload -def s3_upload_file(client, base_dir, file, bucket, key_prefix=None, extra_args=None, max_retry=1): - print(('Uploading file {} to bucket {}.'.format(file, bucket))) - key = file if key_prefix is None else '{}/{}'.format(key_prefix, file) +def s3_upload_file(client, file, bucket, key_prefix=None, extra_args=None, max_retry=1): + key = file if key_prefix is None else f'{key_prefix}/{file}' + error_message = None + for x in range(max_retry): try: - client.upload_file( - os.path.join(base_dir, file), bucket, key, - ExtraArgs=extra_args - ) - print('Upload succeeded') + client.upload_file(file, bucket, key, ExtraArgs=extra_args) return True except Exception as err: - print(('exception while uploading: {}'.format(err))) - print('Retrying upload...') - print('Upload failed') + time.sleep(0.1) # Sleep for 100 milliseconds between retries. + error_message = err + + print(f'Upload failed - Exception while uploading: {error_message}') return False +def _build_file_paths(path_to_files, files_in_path): + """ + Given a path containing files, returns a list of strings representing complete paths to each file. + :param path_to_files: path to the location storing the files to create string paths for + :param files_in_path: list of files that are inside the path_to_files path string + :return: list of fully parsed file path strings from path_to_files path. + """ + parsed_file_paths = [] + + for file_in_path in files_in_path: + complete_file_path = os.path.join(path_to_files, file_in_path) + if os.path.isfile(complete_file_path): + parsed_file_paths.append(complete_file_path) + + return parsed_file_paths + + if __name__ == "__main__": options = parse_args() client = get_client('s3', options.profile) - files_to_upload = get_files_to_upload(options.base_dir, options.file_regex) + files_to_upload = get_files_to_upload(options.base_dir, options.file_regex, options.search_subdirectories) extra_args = json.loads(options.extra_args) if options.extra_args else None print(('Uploading {} files to bucket {}.'.format(len(files_to_upload), options.bucket))) failure = [] success = [] for file in files_to_upload: - if not s3_upload_file(client, options.base_dir, file, options.bucket, options.key_prefix, extra_args, 2): + if not s3_upload_file(client, file, options.bucket, options.key_prefix, extra_args, 2): failure.append(file) else: success.append(file) diff --git a/scripts/ctest/CMakeLists.txt b/scripts/ctest/CMakeLists.txt index 575be53cc7..3a327592db 100644 --- a/scripts/ctest/CMakeLists.txt +++ b/scripts/ctest/CMakeLists.txt @@ -20,33 +20,22 @@ endif() # Tests ################################################################################ -foreach(suite_name ${LY_TEST_GLOBAL_KNOWN_SUITE_NAMES}) - ly_add_pytest( - NAME pytest_sanity_${suite_name}_no_gpu - PATH ${CMAKE_CURRENT_LIST_DIR}/sanity_test.py - TEST_SUITE ${suite_name} - ) - - ly_add_pytest( - NAME pytest_sanity_${suite_name}_requires_gpu - PATH ${CMAKE_CURRENT_LIST_DIR}/sanity_test.py - TEST_SUITE ${suite_name} - TEST_REQUIRES gpu - ) -endforeach() - -# EPB Sanity test is being registered here to validate that the ly_add_editor_python_test function works. -#if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED AND AutomatedTesting IN_LIST LY_PROJECTS_TARGET_NAME) -# ly_add_editor_python_test( -# NAME epb_sanity_smoke_no_gpu -# TEST_PROJECT AutomatedTesting -# PATH ${CMAKE_CURRENT_LIST_DIR}/epb_sanity_test.py -# TEST_SUITE smoke -# TEST_SERIAL TRUE -# RUNTIME_DEPENDENCIES -# AutomatedTesting.Assets -# ) -#endif() +if(PAL_TRAIT_TEST_LYTESTTOOLS_SUPPORTED) + foreach(suite_name ${LY_TEST_GLOBAL_KNOWN_SUITE_NAMES}) + ly_add_pytest( + NAME pytest_sanity_${suite_name}_no_gpu + PATH ${CMAKE_CURRENT_LIST_DIR}/sanity_test.py + TEST_SUITE ${suite_name} + ) + + ly_add_pytest( + NAME pytest_sanity_${suite_name}_requires_gpu + PATH ${CMAKE_CURRENT_LIST_DIR}/sanity_test.py + TEST_SUITE ${suite_name} + TEST_REQUIRES gpu + ) + endforeach() +endif() # add a custom test which makes sure that the test filtering works! diff --git a/scripts/ctest/ctest_entrypoint.sh b/scripts/ctest/ctest_entrypoint.sh index 74e322630c..ea23ae271d 100755 --- a/scripts/ctest/ctest_entrypoint.sh +++ b/scripts/ctest/ctest_entrypoint.sh @@ -14,7 +14,7 @@ # CURRENT_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE:-0}" )" >/dev/null 2>&1 && pwd )" -DEV_DIR="$( dirname "$CURRENT_SCRIPT_DIR" )" +DEV_DIR=$( dirname "$( dirname "$CURRENT_SCRIPT_DIR" )" ) PYTHON=$DEV_DIR/python/python.sh CTEST_SCRIPT=$CURRENT_SCRIPT_DIR/ctest_driver.py diff --git a/scripts/ctest/epb_sanity_test.py b/scripts/ctest/epb_sanity_test.py deleted file mode 100755 index 9a532536b4..0000000000 --- a/scripts/ctest/epb_sanity_test.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -its licensors. - -For complete copyright and license terms please see the LICENSE at the root of this -distribution (the "License"). All use of this software is governed by the License, -or, if provided, by the license below or the license accompanying this file. Do not -remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -""" - -# Sanity test for EditorPythonBindings CTest wrapper - -import azlmbr.framework as framework - -print("EditorPythonBindings CTest Sanity Test") - -# A test should have logic to determine success (zero) or failure (non-zero) and -# return it to the caller. In this sanity test, always return success. -return_code = 0 -framework.Terminate(return_code)