Преглед изворни кода

Initial quality system component (#16514)

* Initial quality system component

Signed-off-by: Alex Peterson <[email protected]>

* Use enum class, remove registry & console members

Signed-off-by: Alex Peterson <[email protected]>

* Fix comment grammar

Signed-off-by: Alex Peterson <[email protected]>

* Fix incorrect console string value parsing

Fix functor pointing to temporary strings
Rename QualityLevels to QualityLevel
Move QualitySystemComponent from Application.cpp to AzFrameworkModule.cpp
Move QualityLevel definition to QualitySystemBus.h
Improved comments per feedback

Signed-off-by: Alex Peterson <[email protected]>

* Update test to use mixed-case

Signed-off-by: Alex Peterson <[email protected]>

* Use ref in FromEnum, from_chars in FromNumber

- Add additional Quality CVAR Group comments
- Use EXPECT_EQ for strings in tests
- Minor format fixes

Signed-off-by: Alex Peterson <[email protected]>

---------

Signed-off-by: Alex Peterson <[email protected]>
Alex Peterson пре 1 година
родитељ
комит
34ccf5e6b4

+ 1 - 1
Code/Framework/AzCore/AzCore/RTTI/TypeInfoSimple.h

@@ -131,7 +131,7 @@ namespace AZ
     /**
     * Use this macro outside a class to allow it to be identified across modules and serialized (in different contexts).
     * The expected input is the class and the assigned uuid as a string or an instance of a uuid.
-    * Note that the AZ_TYPE_INFO_SPECIALIZE does NOT need has to be declared in "namespace AZ".
+    * Note that the AZ_TYPE_INFO_SPECIALIZE does NOT need to be declared in "namespace AZ".
     * It can be declared outside the namespace as mechanism for adding TypeInfo uses function overloading
     * instead of template specialization
     * Example:

+ 3 - 0
Code/Framework/AzFramework/AzFramework/AzFrameworkModule.cpp

@@ -19,6 +19,7 @@
 #include <AzFramework/Input/Contexts/InputContextComponent.h>
 #include <AzFramework/Input/System/InputSystemComponent.h>
 #include <AzFramework/PaintBrush/PaintBrushSystemComponent.h>
+#include <AzFramework/Quality/QualitySystemComponent.h>
 #include <AzFramework/Render/GameIntersectorComponent.h>
 #include <AzFramework/Scene/SceneSystemComponent.h>
 #include <AzFramework/Script/ScriptComponent.h>
@@ -55,6 +56,7 @@ namespace AzFramework
             AzFramework::SceneSystemComponent::CreateDescriptor(),
             AzFramework::StreamingInstall::StreamingInstallSystemComponent::CreateDescriptor(),
             AzFramework::AzFrameworkConfigurationSystemComponent::CreateDescriptor(),
+            AzFramework::QualitySystemComponent::CreateDescriptor(),
 
             AzFramework::OctreeSystemComponent::CreateDescriptor(),
             AzFramework::SpawnableSystemComponent::CreateDescriptor(),
@@ -67,6 +69,7 @@ namespace AzFramework
         return AZ::ComponentTypeList
         {
             azrtti_typeid<AzFramework::OctreeSystemComponent>(),
+            azrtti_typeid<AzFramework::QualitySystemComponent>(),
         };
     }
 }

+ 324 - 0
Code/Framework/AzFramework/AzFramework/Quality/QualityCVarGroup.cpp

@@ -0,0 +1,324 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzFramework/Quality/QualityCVarGroup.h>
+
+#include <AzCore/Settings/SettingsRegistry.h>
+#include <AzCore/Settings/SettingsRegistryVisitorUtils.h>
+#include <AzCore/std/smart_ptr/unique_ptr.h>
+#include <AzCore/Console/IConsole.h>
+#include <AzCore/Memory/SystemAllocator.h>
+#include <AzCore/std/utility/charconv.h>
+
+namespace AzFramework
+{
+    AZ_CLASS_ALLOCATOR_IMPL(QualityCVarGroup, AZ::SystemAllocator);
+
+    QualityCVarGroup::QualityCVarGroup(AZStd::string_view name, AZStd::string_view path)
+        : m_name(name)
+        , m_path(path)
+        , m_numQualityLevels(0)
+        , m_qualityLevel(QualityLevel::LevelFromDeviceRules)
+    {
+        if (auto registry = AZ::SettingsRegistry::Get(); registry != nullptr)
+        {
+            using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
+            using VisitResponse = AZ::SettingsRegistryInterface::VisitResponse;
+
+            // optional default quality level
+            registry->GetObject(m_qualityLevel, FixedValueString::format("%s/Default",
+                m_path.c_str()));
+
+            // optional description
+            registry->Get(m_description, FixedValueString::format("%s/Description",
+                m_path.c_str()));
+
+            // count the number of defined quality levels for error checking
+            AZ::SettingsRegistryVisitorUtils::VisitArray(*registry,
+                [&]([[maybe_unused]] const auto& visitArrayArg)
+                {
+                    m_numQualityLevels++;
+                    return VisitResponse::Continue;
+                },
+                FixedValueString::format("%s/Levels", m_path.c_str()));
+        }
+
+        m_functor = AZStd::make_unique<QualityCVarGroupFunctor>(
+            m_name.c_str(),
+            m_description.c_str(),
+            AZ::ConsoleFunctorFlags::DontReplicate,
+            AZ::AzTypeInfo<QualityLevel>::Uuid(),
+            *this,
+            &QualityCVarGroup::CvarFunctor);
+    }
+
+    QualityCVarGroup::~QualityCVarGroup() = default;
+
+    inline void QualityCVarGroup::CvarFunctor(const AZ::ConsoleCommandContainer& arguments)
+    {
+        StringToValue(arguments);
+    }
+
+    QualityLevel QualityCVarGroup::GetQualityLevel(const AZ::CVarFixedString& value) const
+    {
+        // We don't use AZ::ConsoleTypeHelpers::ToValue because it will print
+        // a warning when it fails to convert a string to int32_t
+        // The QualityLevel strings are user-defined and not known at compile
+        // time so only the pre-defined values can be converted.
+
+        // handle integer values (fast path)
+        QualityLevel qualityLevel = FromNumber(value);
+
+        if (qualityLevel == QualityLevel::Invalid)
+        {
+            // handle enum pre-defined values 
+            qualityLevel = FromEnum(value);
+        }
+
+        if (qualityLevel == QualityLevel::Invalid)
+        {
+            // handle user-defined level name values
+            qualityLevel = FromName(value);
+        }
+
+        return qualityLevel;
+    }
+
+    QualityLevel QualityCVarGroup::FromNumber(const AZ::CVarFixedString& value) const
+    {
+        int32_t intValue{ 0 };
+        auto result = AZStd::from_chars(value.begin(), value.end(), intValue);
+
+        // don't allow values outside the known quality level ranges 
+        constexpr int32_t minValue = static_cast<int32_t>(QualityLevel::LevelFromDeviceRules);
+        if (result.ec != AZStd::errc() || intValue >= m_numQualityLevels || intValue < minValue)
+        {
+            return QualityLevel::Invalid;
+        }
+
+        return QualityLevel(intValue);
+    }
+
+    QualityLevel QualityCVarGroup::FromEnum(const AZ::CVarFixedString& value) const
+    {
+        using EnumMemberPair = typename AZ::AzEnumTraits<QualityLevel>::MembersArrayType::value_type;
+        auto findEnumOptionValue = [&value](EnumMemberPair enumPair)
+        {
+            // case-insensitive comparison
+            constexpr bool caseSensitive{ false };
+            return AZ::StringFunc::Equal(value, enumPair.m_string, caseSensitive);
+        };
+        if (auto foundIt = AZStd::ranges::find_if(AZ::AzEnumTraits<QualityLevel>::Members,
+            findEnumOptionValue); foundIt != AZ::AzEnumTraits<QualityLevel>::Members.end())
+        {
+            return foundIt->m_value;
+        }
+
+        return QualityLevel::Invalid;
+    }
+
+    QualityLevel QualityCVarGroup::FromName(const AZ::CVarFixedString& value) const
+    {
+        using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
+        using VisitResponse = AZ::SettingsRegistryInterface::VisitResponse;
+
+        auto registry = AZ::SettingsRegistry::Get();
+        if (registry == nullptr)
+        {
+            return QualityLevel::Invalid;
+        }
+
+        QualityLevel qualityLevel{ QualityLevel::Invalid };
+
+        // visit each user-defined level name and perform a case-insensitive comparison
+        int32_t arrayIndex{ 0 };
+        auto callback = [&](const auto& visitArrayArg)
+        {
+            FixedValueString qualityLevelName;
+            if (registry->Get(qualityLevelName, visitArrayArg.m_jsonKeyPath))
+            {
+                // case-insensitive comparison
+                constexpr bool caseSensitive{ false };
+                if (AZ::StringFunc::Equal(qualityLevelName, value, caseSensitive))
+                {
+                    qualityLevel = QualityLevel(arrayIndex);
+                    return VisitResponse::Done;
+                }
+            }
+            arrayIndex++;
+            return VisitResponse::Continue;
+        };
+        AZ::SettingsRegistryVisitorUtils::VisitArray(*registry, callback,
+            FixedValueString::format("%s/Levels", m_path.c_str()));
+
+        return qualityLevel;
+    }
+
+    void QualityCVarGroup::LoadQualityLevel(QualityLevel qualityLevel)
+    {
+        using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
+        using VisitResponse = AZ::SettingsRegistryInterface::VisitResponse;
+        using Type = AZ::SettingsRegistryInterface::Type;
+
+        auto registry = AZ::SettingsRegistry::Get();
+        if (registry == nullptr)
+        {
+            return;
+        }
+
+        auto callback = [&](const auto& visitArgs)
+        {
+            AZStd::string_view command = visitArgs.m_fieldName;
+
+            if (visitArgs.m_type == Type::Object ||
+                visitArgs.m_type == Type::Null ||
+                visitArgs.m_type == Type::NoType)
+            {
+                AZ_Warning("QualityCVarGroup", false,
+                    "Invalid type for %.*s, valid types are array, bool, string, and number.",
+                    AZ_STRING_ARG(command));
+                return VisitResponse::Skip;
+            }
+
+            AZ::PerformCommandResult result{};
+            if (visitArgs.m_type == Type::Array)
+            {
+                // the qualityLevel is the index containing the value to use for
+                // the CVAR command, however, if the array does not contain the
+                // qualityLevel index, use the highest available
+                QualityLevel currentQualityLevel{ QualityLevel::LevelFromDeviceRules };
+                auto applyQualityLevel = [&](const auto& visitArrayArg)
+                {
+                    currentQualityLevel++;
+                    if (currentQualityLevel == qualityLevel)
+                    {
+                        result = PerformConsoleCommand(command, visitArrayArg.m_jsonKeyPath);
+                        return VisitResponse::Done;
+                    }
+                    return VisitResponse::Continue;
+                };
+
+                AZ::SettingsRegistryVisitorUtils::VisitArray(*registry,
+                    applyQualityLevel, visitArgs.m_jsonKeyPath);
+
+                if (currentQualityLevel == QualityLevel::LevelFromDeviceRules)
+                {
+                    AZ_Error("QualityCVarGroup", false,
+                        "No valid value found for %.*s, the array is empty.",
+                        AZ_STRING_ARG(visitArgs.m_jsonKeyPath));
+                }
+                else if (!result)
+                {
+                    // the requested qualityLevel index wasn't found, use highest available 
+                    PerformConsoleCommand(command, FixedValueString::format("%.*s/%d",
+                        AZ_STRING_ARG(visitArgs.m_jsonKeyPath), currentQualityLevel));
+                }
+            }
+            else
+            {
+                PerformConsoleCommand(command, visitArgs.m_jsonKeyPath);
+            }
+
+            return VisitResponse::Continue;
+        };
+        auto key = FixedValueString::format("%s/Settings", m_path.c_str());
+        AZ::SettingsRegistryVisitorUtils::VisitObject(*registry, callback, key);
+    }
+
+    AZ::PerformCommandResult QualityCVarGroup::PerformConsoleCommand(AZStd::string_view command, AZStd::string_view key)
+    {
+        using Type = AZ::SettingsRegistryInterface::Type;
+
+        auto registry = AZ::SettingsRegistry::Get();
+        auto console = AZ::Interface<AZ::IConsole>::Get();
+        if (!registry || !console)
+        {
+            return AZ::Failure("Missing required console or settings registry");
+        }
+
+        AZStd::string stringValue;
+        switch (registry->GetType(key))
+        {
+        case Type::FloatingPoint:
+            if (double value; registry->Get(value, key))
+            {
+                stringValue = AZ::ConsoleTypeHelpers::ToString(value);
+            }
+            break;
+        case Type::Boolean:
+            if (bool value; registry->Get(value, key))
+            {
+                stringValue = AZ::ConsoleTypeHelpers::ToString(value);
+            }
+            break;
+        case Type::Integer:
+            if (AZ::s64 value; registry->Get(value, key))
+            {
+                stringValue = AZ::ConsoleTypeHelpers::ToString(value);
+            }
+            break;
+        case Type::String:
+        default:
+            {
+                registry->Get(stringValue, key);
+            }
+            break;
+        }
+
+        if (!stringValue.empty())
+        {
+            return console->PerformCommand(command, { stringValue }, AZ::ConsoleSilentMode::Silent);
+        }
+
+        return AZ::Failure(AZStd::string::format("Failed to stringify %.*s value, or it's empty.",
+            AZ_STRING_ARG(command)));
+    }
+
+    bool QualityCVarGroup::StringToValue(const AZ::ConsoleCommandContainer& arguments)
+    {
+        auto registry = AZ::SettingsRegistry::Get();
+        if (registry == nullptr)
+        {
+            return false;
+        }
+
+        if (!arguments.empty())
+        {
+            AZ::CVarFixedString value{ arguments.front() };
+
+            // GetQualityLevel will return QualityLevel::Invalid if unable
+            // to convert the provided value to a valid QualityLevel
+            QualityLevel qualityLevel = GetQualityLevel(value);
+
+            if (qualityLevel == QualityLevel::LevelFromDeviceRules)
+            {
+                // TODO use device rules
+
+                // fall back to default
+                using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
+                registry->GetObject(qualityLevel, FixedValueString::format("%s/Default", m_path.c_str()));
+            }
+
+            if (qualityLevel != QualityLevel::Invalid)
+            {
+                m_qualityLevel = qualityLevel;
+                LoadQualityLevel(m_qualityLevel);
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    void QualityCVarGroup::ValueToString(AZ::CVarFixedString& outString) const
+    {
+        outString = AZ::ConsoleTypeHelpers::ToString(m_qualityLevel);
+    }
+} // namespace AzFramework
+

+ 94 - 0
Code/Framework/AzFramework/AzFramework/Quality/QualityCVarGroup.h

@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/Memory/Memory_fwd.h>
+#include <AzCore/Console/IConsole.h>
+#include <AzCore/Console/ConsoleFunctor.h>
+#include <AzCore/std/string/string.h>
+#include <AzCore/std/string/string_view.h>
+
+#include <AzFramework/Quality/QualitySystemBus.h>
+
+namespace AzFramework
+{
+    // QualityCVarGroup wraps a QualityLevel CVAR for a quality group that, when set,
+    // iterates over all settings in the group and performs console commands to
+    // apply the settings for the requested quality level.
+    // The general format of a quality group entry in the Settings Registry is:
+    // {
+    //     "O3DE" : {
+    //         "Quality" : {
+    //             "Groups" : {
+    //                 "<group CVAR>" : {
+    //                      "Description" : "<optional description>",
+    //                      "Levels" : [ "<quality level 0>", "<quality level n>" ],
+    //                      "Default" : "<default quality level>",
+    //                      "Settings" : {
+    //                          "<setting CVAR>" : [<level 0 value>, <level n value> ]
+    //                      }
+    //                  }
+    //             }
+    //         }
+    //     }
+    // }
+    //    
+    // QualityCVarGroup only creates a CVAR for the quality group itself, it does not
+    // create CVARs for any entries in the quality groups "Settings" object.
+    // Quality levels are assumed to be ordered low to high as in the example below.
+    //
+    // Example:
+    // A q_shadows group CVAR exists with 4 levels (low=0,medium=1,high=2,veryhigh=3)
+    // When the level of q_shadows is set to 2 LoadQualityLevel will apply all
+    // console settings defined in the SettingsRegistry for that
+    // group at level 2 (high).
+    //
+    class QualityCVarGroup
+    {
+    public:
+        AZ_CLASS_ALLOCATOR_DECL;
+
+        QualityCVarGroup(AZStd::string_view name, AZStd::string_view path);
+        ~QualityCVarGroup();
+
+        explicit inline operator QualityLevel() const { return m_qualityLevel; }
+
+        //! required to set the value
+        inline void CvarFunctor(const AZ::ConsoleCommandContainer& arguments);
+
+        //! required to set the value
+        bool StringToValue(const AZ::ConsoleCommandContainer& arguments);
+
+        //! required to get the current value
+        void ValueToString(AZ::CVarFixedString& outString) const;
+
+        //! load the requested quality level CVar settings
+        void LoadQualityLevel(QualityLevel qualityLevel);
+
+        QualityLevel GetQualityLevel(const AZ::CVarFixedString& value) const;
+
+    private:
+        AZ_DISABLE_COPY(QualityCVarGroup);
+
+        QualityLevel FromNumber(const AZ::CVarFixedString& value) const;
+        QualityLevel FromEnum(const AZ::CVarFixedString& value) const;
+        QualityLevel FromName(const AZ::CVarFixedString& value) const;
+
+        AZ::PerformCommandResult PerformConsoleCommand(AZStd::string_view command, AZStd::string_view key);
+
+        inline static constexpr bool ReplicateCommand = true;
+        using QualityCVarGroupFunctor = AZ::ConsoleFunctor<QualityCVarGroup, ReplicateCommand>;
+        AZStd::string m_description;
+        AZStd::string m_name;
+        AZStd::unique_ptr<QualityCVarGroupFunctor> m_functor;
+        AZStd::string m_path;
+        int32_t m_numQualityLevels{ 0 };
+        QualityLevel m_qualityLevel = QualityLevel::DefaultQualityLevel;
+    };
+} // namespace AzFramework
+

+ 46 - 0
Code/Framework/AzFramework/AzFramework/Quality/QualitySystemBus.h

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/EBus/EBus.h>
+#include <AzCore/Preprocessor/Enum.h>
+#include <AzCore/std/string/string_view.h>
+
+namespace AzFramework
+{
+    AZ_ENUM_CLASS(QualityLevel,
+        (Invalid, -2),
+        // When loading quality group settings using a level value of LevelFromDeviceRules
+        // the device rules will be used to determine the quality level to use.
+        // Quality levels are assumed to be ordered low to high with 0 being the lowest
+        // quality.
+        (LevelFromDeviceRules, -1),
+        (DefaultQualityLevel, 0)
+    );
+    AZ_TYPE_INFO_SPECIALIZE(QualityLevel, "{9AABD1B2-D433-49FE-A89D-2BEF09A252C0}");
+    AZ_DEFINE_ENUM_ARITHMETIC_OPERATORS(QualityLevel);
+    AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(QualityLevel);
+
+    class QualitySystemEvents
+        : public AZ::EBusTraits
+    {
+    public:
+        using Bus = AZ::EBus<QualitySystemEvents>;
+
+        // Loads the default quality group if one is defined in /O3DE/Quality/DefaultGroup
+        // If the requested QualityLevel is 0 or higher, settings for that level will be loaded.
+        // If the requested QualityLevel is LevelFromDeviceRules, device rules will be used to
+        // determine the quality level, and if no device rule match is found the default
+        // level for that group will be used if it has one.
+        //
+        // @param level Optional quality level to load. If LevelFromDeviceRules, the level will be
+        //              determined from device rules. If there is no matching device rule, the
+        //              default level for the group will be used.
+        virtual void LoadDefaultQualityGroup(QualityLevel level = QualityLevel::LevelFromDeviceRules) = 0;
+    };
+} //AzFramework

+ 127 - 0
Code/Framework/AzFramework/AzFramework/Quality/QualitySystemComponent.cpp

@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzFramework/Quality/QualitySystemComponent.h>
+#include <AzFramework/Quality/QualityCVarGroup.h>
+
+#include <AzCore/Console/IConsole.h>
+#include <AzCore/Preprocessor/EnumReflectUtils.h> 
+#include <AzCore/Serialization/EditContext.h>
+#include <AzCore/Settings/SettingsRegistry.h>
+#include <AzCore/Settings/SettingsRegistryVisitorUtils.h>
+#include <AzCore/StringFunc/StringFunc.h>
+
+
+namespace AzFramework
+{
+    inline constexpr const char* QualitySettingsGroupsRootKey = "/O3DE/Quality/Groups";
+    inline constexpr const char* QualitySettingsDefaultGroupKey = "/O3DE/Quality/DefaultGroup";
+
+    // constructor and destructor defined here to prevent compiler errors
+    // if default constructor/destructor is defined in header because of
+    // the member vector of unique_ptrs
+    QualitySystemComponent::QualitySystemComponent() = default;
+    QualitySystemComponent::~QualitySystemComponent() = default;
+
+    AZ_ENUM_DEFINE_REFLECT_UTILITIES(QualityLevel);
+
+    void QualitySystemComponent::Reflect(AZ::ReflectContext* context)
+    {
+        if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
+        {
+            QualityLevelReflect(*serializeContext);
+
+            serializeContext->Class<QualitySystemComponent, AZ::Component>();
+
+            if (AZ::EditContext* editContext = serializeContext->GetEditContext())
+            {
+                editContext->Class<QualitySystemComponent>(
+                    "AzFramework Quality Component", "System component responsible for quality settings")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                    ->Attribute(AZ::Edit::Attributes::Category, "Editor")
+                    ;
+            }
+        }
+    }
+
+    void QualitySystemComponent::Activate()
+    {
+        auto registry = AZ::SettingsRegistry::Get();
+        AZ_Assert(registry, "QualitySystemComponent requires a SettingsRegistry but no instance has been created.");
+
+        auto console = AZ::Interface<AZ::IConsole>::Get();
+        AZ_Assert(console, "QualitySystemComponent requires an IConsole interface but no instance has been created.");
+
+        if (registry && console)
+        {
+            registry->Get(m_defaultGroupName, QualitySettingsDefaultGroupKey);
+            RegisterCvars();
+            QualitySystemEvents::Bus::Handler::BusConnect();
+        }
+    }
+
+    void QualitySystemComponent::Deactivate()
+    {
+        QualitySystemEvents::Bus::Handler::BusDisconnect();
+        m_settingsGroupCVars.clear();
+    }
+
+    void QualitySystemComponent::LoadDefaultQualityGroup(QualityLevel qualityLevel)
+    {
+        if (m_defaultGroupName.empty())
+        {
+            AZ_Warning("QualitySystemComponent", false,
+                "No default quality settings loaded because no default group name defined at %s",
+                QualitySettingsDefaultGroupKey);
+        }
+        else if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
+        {
+            console->PerformCommand(AZStd::string_view(m_defaultGroupName),
+                { AZ::ConsoleTypeHelpers::ToString(qualityLevel) }, AZ::ConsoleSilentMode::Silent);
+        }
+    }
+
+    void QualitySystemComponent::RegisterCvars()
+    {
+        // walk the quality groups in the settings registry and create cvars for every group
+        auto callback = [&](const AZ::SettingsRegistryInterface::VisitArgs& visitArgs)
+        {
+            if (visitArgs.m_type != AZ::SettingsRegistryInterface::Type::Object)
+            {
+                AZ_Warning(
+                    "QualitySystemComponent",
+                    false,
+                    "Skipping quality setting group entry '%.*s' that is not an object type.",
+                    AZ_STRING_ARG(visitArgs.m_fieldName));
+                return AZ::SettingsRegistryInterface::VisitResponse::Skip;
+            }
+
+            m_settingsGroupCVars.push_back(AZStd::make_unique<QualityCVarGroup>(visitArgs.m_fieldName, visitArgs.m_jsonKeyPath));
+            return AZ::SettingsRegistryInterface::VisitResponse::Continue;
+        };
+
+        auto registry = AZ::SettingsRegistry::Get();
+        AZ::SettingsRegistryVisitorUtils::VisitObject(*registry, callback, QualitySettingsGroupsRootKey);
+    }
+
+    void QualitySystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+    {
+        provided.push_back(AZ_CRC("QualitySystemComponentService"));
+    }
+
+    void QualitySystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
+    {
+        incompatible.push_back(AZ_CRC("QualitySystemComponentService"));
+    }
+
+    void QualitySystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent)
+    {
+    }
+
+} // AzFramework
+

+ 52 - 0
Code/Framework/AzFramework/AzFramework/Quality/QualitySystemComponent.h

@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#pragma once
+
+#include <AzCore/Component/Component.h>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/smart_ptr/unique_ptr.h>
+#include <AzFramework/Quality/QualitySystemBus.h>
+
+namespace AzFramework
+{
+    class QualityCVarGroup;
+
+    // QualitySystemComponent manages quality groups, levels and cvar settings
+    // stored in the SettingsRegistry
+    class QualitySystemComponent final
+        : public AZ::Component
+        , public QualitySystemEvents::Bus::Handler
+    {
+    public:
+        AZ_COMPONENT(QualitySystemComponent, "{CA269E6A-A420-4B68-93E9-2E09A604D29A}", AZ::Component);
+
+        QualitySystemComponent();
+        ~QualitySystemComponent() override;
+
+        AZ_DISABLE_COPY(QualitySystemComponent);
+
+        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided);
+        static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible);
+        static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent);
+        static void Reflect(AZ::ReflectContext* context);
+
+        // AZ::Component
+        void Activate() override;
+        void Deactivate() override;
+
+        // AzFramework::QualitySystemBus
+        void LoadDefaultQualityGroup(QualityLevel qualityLevel = QualityLevel::LevelFromDeviceRules) override;
+
+    private:
+        void RegisterCvars();
+
+        AZStd::string m_defaultGroupName;
+        AZStd::vector<AZStd::unique_ptr<QualityCVarGroup>> m_settingsGroupCVars;
+    };
+} // AzFramework
+

+ 5 - 0
Code/Framework/AzFramework/AzFramework/azframework_files.cmake

@@ -328,6 +328,11 @@ set(FILES
     Process/ProcessUtils.h
     ProjectManager/ProjectManager.h
     ProjectManager/ProjectManager.cpp
+    Quality/QualityCVarGroup.cpp
+    Quality/QualityCVarGroup.h
+    Quality/QualitySystemComponent.cpp
+    Quality/QualitySystemComponent.h
+    Quality/QualitySystemBus.h
     Render/GameIntersectorComponent.h
     Render/GameIntersectorComponent.cpp
     Render/GeometryIntersectionBus.h

+ 242 - 0
Code/Framework/AzFramework/Tests/QualitySystemComponentTests.cpp

@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzCore/UnitTest/TestTypes.h>
+#include <AzCore/Console/Console.h>
+#include <AzCore/Settings/SettingsRegistry.h>
+#include <AzCore/Settings/SettingsRegistryImpl.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/UserSettings/UserSettingsComponent.h>
+#include <AzFramework/Application/Application.h>
+#include <AzFramework/Quality/QualitySystemComponent.h>
+#include <AzFramework/Quality/QualityCVarGroup.h>
+
+namespace UnitTest
+{
+    class QualitySystemComponentTestFixture : public LeakDetectionFixture
+    {
+    public:
+        QualitySystemComponentTestFixture()
+            : LeakDetectionFixture()
+        {
+        }
+
+    protected:
+        void SetUp() override
+        {
+            using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
+
+            m_settingsRegistry = AZStd::make_unique<AZ::SettingsRegistryImpl>();
+            AZ::SettingsRegistry::Register(m_settingsRegistry.get());
+
+            m_application = AZStd::make_unique<AzFramework::Application>();
+
+            // setup the runtime paths for the FileTagComponent
+            auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
+            AZ::IO::FixedMaxPath enginePath;
+            m_settingsRegistry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
+            m_settingsRegistry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
+            AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*m_settingsRegistry);
+
+            m_console = AZ::Interface<AZ::IConsole>::Get();
+        }
+
+        void TearDown() override
+        {
+            m_application->Stop();
+            m_application.reset();
+
+            AZ::SettingsRegistry::Unregister(m_settingsRegistry.get());
+            m_settingsRegistry.reset();
+        }
+
+        void StartApplicationWithSettings(AZStd::string_view settings)
+        {
+            auto result = m_settingsRegistry->MergeSettings(settings,
+                AZ::SettingsRegistryInterface::Format::JsonMergePatch,
+                "");
+            ASSERT_TRUE(result);
+
+            // When the application starts and activates the QualitySystemComponent
+            // it will register CVARS based on what is in the registry
+            AZ::ComponentApplication::Descriptor desc;
+            AZ::ComponentApplication::StartupParameters startupParameters;
+            startupParameters.m_loadSettingsRegistry = false;
+            startupParameters.m_loadAssetCatalog = false;
+            m_application->Start(desc, startupParameters);
+
+            // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
+            // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
+            // in the unit tests.
+            AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
+        }
+
+        AZ::IConsole* m_console;
+        AZStd::unique_ptr<AzFramework::Application> m_application;
+        AZStd::unique_ptr<AZ::SettingsRegistryInterface> m_settingsRegistry;
+    };
+
+    TEST_F(QualitySystemComponentTestFixture, QualitySystem_Registers_Group_CVars)
+    {
+        // when the quality system component registers group cvars
+        StartApplicationWithSettings(R"(
+            {
+                "O3DE": {
+                    "Quality": {
+                        "Groups": {
+                            "q_test": {
+                                "Levels": [ "low", "high" ],
+                                "Default": 1,
+                                "Description": "q_test quality group",
+                                "Settings": {
+                                    "q_test_sub": [0,1]
+                                }
+                            },
+                            "q_test_sub": {
+                                "Levels": [ "low", "high" ],
+                                "Default": 0,
+                                "Description": "q_test_sub quality group",
+                                "Settings": {
+                                    "a_cvar": [123,234]
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            )");
+
+        // expect the cvars are created with their default values
+        auto value = AzFramework::QualityLevel::LevelFromDeviceRules;
+        EXPECT_EQ(m_console->GetCvarValue("q_test", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel{1});
+
+        EXPECT_EQ(m_console->GetCvarValue("q_test_sub", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel::DefaultQualityLevel);
+    }
+
+    AZ_CVAR(int32_t, a_setting, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Example integer setting 1");
+    AZ_CVAR(AZ::CVarFixedString, b_setting, "default", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Example string setting 2");
+    AZ_CVAR(int32_t, c_setting, -1, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Example integer setting 3");
+    AZ_CVAR(int32_t, d_setting, -2, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Example integer setting 4");
+
+    TEST_F(QualitySystemComponentTestFixture, QualitySystem_Loads_Group_Level)
+    {
+        // when the quality system component registers group cvars
+        StartApplicationWithSettings(R"(
+            {
+                "O3DE": {
+                    "Quality": {
+                        "DefaultGroup":"q_test",
+                        "Groups": {
+                            "q_test": {
+                                "Levels": [ "low", "medium", "high", "veryhigh"],
+                                "Default": 2,
+                                "Description": "q_test quality group",
+                                "Settings": {
+                                    "a_setting": [0,1,2,3],
+                                    "b_setting": ["a","b","c","d"],
+                                    "q_test_sub": [0,1,1,1]
+                                }
+                            },
+                            "q_test_sub": {
+                                "Levels": [ "low", "high" ],
+                                "Default": "DefaultQualityLevel",
+                                "Description": "q_test_sub quality group",
+                                "Settings": {
+                                    "c_setting": [123,234],
+                                    "d_setting": [42] // test missing high level setting
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            )");
+
+        auto value = AzFramework::QualityLevel::LevelFromDeviceRules;
+        int32_t intValue = -42;
+        AZ::CVarFixedString stringValue;
+
+        // expect the value defaults
+        EXPECT_EQ(m_console->GetCvarValue("a_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 0);
+        EXPECT_EQ(m_console->GetCvarValue("b_setting", stringValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(stringValue, "default");
+        EXPECT_EQ(m_console->GetCvarValue("c_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, -1);
+        EXPECT_EQ(m_console->GetCvarValue("d_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, -2);
+
+        // when the default group is loaded
+        AzFramework::QualitySystemEvents::Bus::Broadcast(
+            &AzFramework::QualitySystemEvents::LoadDefaultQualityGroup,
+            AzFramework::QualityLevel::LevelFromDeviceRules);
+
+        // expect the values are set based on the default for q_test which is 2 
+        EXPECT_EQ(m_console->GetCvarValue("q_test", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel{2});
+
+        EXPECT_EQ(m_console->GetCvarValue("a_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 2);
+
+        EXPECT_EQ(m_console->GetCvarValue("b_setting", stringValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(stringValue, "c");
+
+        EXPECT_EQ(m_console->GetCvarValue("q_test_sub", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel{1});
+
+        EXPECT_EQ(m_console->GetCvarValue("c_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 234);
+
+        EXPECT_EQ(m_console->GetCvarValue("d_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 42);
+
+        // when the group level 1 is loaded ("medium" which is "high" for q_test_sub)
+        m_console->PerformCommand("q_test", { "1" });
+
+        // expect the values are set based on the group level settings
+        EXPECT_EQ(m_console->GetCvarValue("a_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 1);
+
+        EXPECT_EQ(m_console->GetCvarValue("b_setting", stringValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(stringValue, "b");
+
+        EXPECT_EQ(m_console->GetCvarValue("q_test_sub", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel{1});
+
+        EXPECT_EQ(m_console->GetCvarValue("c_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 234);
+
+        // d_settings doesn't specify a value for "high" so it should use highest available setting
+        // which is "low" -> 42
+        EXPECT_EQ(m_console->GetCvarValue("d_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 42);
+
+        // when the group level 0 is loaded (low) using mixed-case name 
+        m_console->PerformCommand("q_test", { "LoW" });
+
+        // settings at index 0 are correctly loaded
+        EXPECT_EQ(m_console->GetCvarValue("a_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 0);
+
+        EXPECT_EQ(m_console->GetCvarValue("b_setting", stringValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(stringValue, "a");
+
+        EXPECT_EQ(m_console->GetCvarValue("q_test_sub", value), AZ::GetValueResult::Success);
+        EXPECT_EQ(value, AzFramework::QualityLevel{0});
+
+        EXPECT_EQ(m_console->GetCvarValue("c_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 123);
+
+        EXPECT_EQ(m_console->GetCvarValue("d_setting", intValue), AZ::GetValueResult::Success);
+        EXPECT_EQ(intValue, 42);
+    }
+} // namespace UnitTest
+

+ 1 - 0
Code/Framework/AzFramework/Tests/frameworktests_files.cmake

@@ -44,4 +44,5 @@ set(FILES
     PaintBrush/PaintBrushPaintLocationTests.cpp
     PaintBrush/PaintBrushPaintSettingsTests.cpp
     PaintBrush/PaintBrushSmoothLocationTests.cpp
+    QualitySystemComponentTests.cpp
 )

+ 7 - 0
Code/Legacy/CrySystem/SystemInit.cpp

@@ -33,6 +33,7 @@
 
 #include <AzFramework/IO/LocalFileIO.h>
 #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
+#include <AzFramework/Quality/QualitySystemBus.h>
 
 #include "AZCoreLogSink.h"
 #include <AzCore/Component/ComponentApplicationBus.h>
@@ -1112,6 +1113,12 @@ bool CSystem::Init(const SSystemInitParams& startupParams)
 
     InlineInitializationProcessing("CSystem::Init End");
 
+    // All CVARs should now be registered, load and apply quality settings for the default quality group
+    // using device rules to auto-detected the correct quality level 
+    AzFramework::QualitySystemEvents::Bus::Broadcast(
+        &AzFramework::QualitySystemEvents::LoadDefaultQualityGroup,
+        AzFramework::QualityLevel::LevelFromDeviceRules);
+
     // Send out EBus event
     EBUS_EVENT(CrySystemEventBus, OnCrySystemInitialized, *this, startupParams);
 

+ 38 - 0
Gems/Atom/Feature/Common/Registry/quality.setreg

@@ -0,0 +1,38 @@
+{
+    "O3DE": {
+        "Quality": {
+            "Groups": {
+                "q_general": {
+                    "Settings": {
+                        "q_graphics": [ 0, 1, 2, 3 ] // map q_general levels 1 to 1 with graphics levels
+                    }
+                },
+                "q_graphics": {
+                    "Description": "Graphics quality settings.  0 : Low, 1 : Medium, 2 : High, 3 : VeryHigh",
+                    "Levels": [
+                        "Low",
+                        "Medium",
+                        "High",
+                        "VeryHigh"
+                    ],
+                    "Default": 3,
+                    "Settings": {
+                        "q_shadows": [ 0, 1, 2, 3 ]
+                    }
+                },
+                "q_shadows": {
+                    "Description": "Shadow quality settings.  0 : Low, 1 : Medium, 2 : High, 3 : VeryHigh",
+                    "Levels": [
+                        "Low",
+                        "Medium",
+                        "High",
+                        "VeryHigh"
+                    ],
+                    "Settings": {
+                        // shadows console variable settings
+                    }
+                }
+            }
+        }
+    }
+}

+ 19 - 0
Registry/quality.setreg

@@ -0,0 +1,19 @@
+{
+    "O3DE": {
+        "Quality": {
+            "DefaultGroup": "q_general",
+            "Groups": {
+                "q_general": {
+                    "Description" : "Default quality group. 0 : Low, 1 : Medium, 2 : High, 3 : VeryHigh",
+                    "Levels": [
+                        "Low",
+                        "Medium",
+                        "High",
+                        "VeryHigh"
+                    ],
+                    "Default": 3
+                }
+            }
+        }
+    }
+}