Merge branch 'development' into atom_cpu_profiler_gem_promotion

Signed-off-by: AMZN-ScottR <24445312+AMZN-ScottR@users.noreply.github.com>
This commit is contained in:
AMZN-ScottR
2021-10-19 10:58:35 -07:00
352 changed files with 3789 additions and 643927 deletions
@@ -54,6 +54,30 @@ def get_mesh_node_names(sceneGraph):
return meshDataList, paths
def add_material_component(entity_id):
# Create an override AZ::Render::EditorMaterialComponent
editor_material_component = azlmbr.entity.EntityUtilityBus(
azlmbr.bus.Broadcast,
"GetOrAddComponentByTypeName",
entity_id,
"EditorMaterialComponent")
# this fills out the material asset to a known product AZMaterial asset relative path
json_update = json.dumps({
"Controller": { "Configuration": { "materials": [
{
"Key": {},
"Value": { "MaterialAsset":{
"assetHint": "materials/basic_grey.azmaterial"
}}
}]
}}
});
result = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "UpdateComponentForEntity", entity_id, editor_material_component, json_update)
if not result:
raise RuntimeError("UpdateComponentForEntity for editor_material_component failed")
def update_manifest(scene):
import json
import uuid, os
@@ -75,6 +99,7 @@ def update_manifest(scene):
created_entities = []
previous_entity_id = azlmbr.entity.InvalidEntityId
first_mesh = True
# Loop every mesh node in the scene
for activeMeshIndex in range(len(mesh_name_list)):
@@ -112,6 +137,11 @@ def update_manifest(scene):
if not result:
raise RuntimeError("UpdateComponentForEntity failed for Mesh component")
# an example of adding a material component to override the default material
if previous_entity_id is not None and first_mesh:
first_mesh = False
add_material_component(entity_id)
# Get the transform component
transform_component = azlmbr.entity.EntityUtilityBus(azlmbr.bus.Broadcast, "GetOrAddComponentByTypeName", entity_id, "27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0")
@@ -204,7 +204,7 @@ namespace PythonCoverage
return coveringModuleOutputNames;
}
void PythonCoverageEditorSystemComponent::OnStartExecuteByFilenameAsTest(AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector<AZStd::string_view>& args)
void PythonCoverageEditorSystemComponent::OnStartExecuteByFilenameAsTest([[maybe_unused]]AZStd::string_view filename, AZStd::string_view testCase, [[maybe_unused]] const AZStd::vector<AZStd::string_view>& args)
{
if (m_coverageState == CoverageState::Disabled)
{
@@ -226,8 +226,7 @@ namespace PythonCoverage
return;
}
const AZStd::string scriptName = AZ::IO::Path(filename).Stem().Native();
const auto coverageFile = m_coverageDir / AZStd::string::format("%s.pycoverage", scriptName.c_str());
const auto coverageFile = m_coverageDir / AZStd::string::format("%.*s.pycoverage", AZ_STRING_ARG(testCase));
// If this is a different python script we clear the existing entity components and start afresh
if (m_coverageFile != coverageFile)
@@ -51,7 +51,7 @@ def launch_and_validate_results(request, test_directory, editor, editor_script,
logger.debug("Running automated test: {}".format(editor_script))
editor.args.extend(["--skipWelcomeScreenDialog", "--regset=/Amazon/Settings/EnableSourceControl=false",
"--regset=/Amazon/Preferences/EnablePrefabSystem=false", run_python, test_case,
f"--pythontestcase={request.node.originalname}", "--runpythonargs", " ".join(cfg_args)])
f"--pythontestcase={request.node.name}", "--runpythonargs", " ".join(cfg_args)])
if auto_test_mode:
editor.args.extend(["--autotest_mode"])
if null_renderer:
@@ -126,7 +126,7 @@ def ForceRegion_LinearDampingForceOnRigidBodies():
# Constants
CLOSE_ENOUGH = 0.001
TIME_OUT = 3.0
TIME_OUT = 10.0
INITIAL_VELOCITY = azmath.Vector3(0.0, 0.0, -10.0)
# 1) Open level / Enter game mode
@@ -90,7 +90,7 @@ class TestAutomationBase:
editor_starttime = time.time()
self.logger.debug("Running automated test")
testcase_module_filepath = self._get_testcase_module_filepath(testcase_module)
pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={request.node.originalname}"]
pycmd = ["--runpythontest", testcase_module_filepath, f"-pythontestcase={request.node.name}"]
if use_null_renderer:
pycmd += ["-rhi=null"]
if batch_mode:
@@ -1,44 +0,0 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
UI Apps: AutomatedTesting.GameLauncher
Launch AutomatedTesting.GameLauncher with Simple level
Test should run in both gpu and non gpu
"""
import pytest
import psutil
import ly_test_tools.environment.waiter as waiter
import editor_python_test_tools.hydra_test_utils as editor_test_utils
from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole
from ly_remote_console.remote_console_commands import (
send_command_and_expect_response as send_command_and_expect_response,
)
@pytest.mark.parametrize("launcher_platform", ["windows"])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("level", ["Simple"])
@pytest.mark.SUITE_smoke
class TestRemoteConsoleLoadLevelWorks(object):
@pytest.fixture
def remote_console_instance(self, request):
console = RemoteConsole()
def teardown():
if console.connected:
console.stop()
request.addfinalizer(teardown)
return console
def test_RemoteConsole_LoadLevel_Works(self, launcher, level, remote_console_instance, launcher_platform):
expected_lines = ['Level system is loading "Simple"']
editor_test_utils.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, null_renderer=True)
@@ -17,13 +17,6 @@
"Gems/PhysicsEntities"
]
},
"PhysXSamples":
{
"SourcePaths":
[
"Gems/PhysXSamples"
]
},
"PrimitiveAssets":
{
"SourcePaths":
+4 -3
View File
@@ -513,7 +513,7 @@ public:
QString m_appRoot;
QString m_logFile;
QString m_pythonArgs;
QString m_pythontTestCase;
QString m_pythonTestCase;
QString m_execFile;
QString m_execLineCmd;
@@ -562,7 +562,7 @@ public:
const std::vector<std::pair<CommandLineStringOption, QString&> > stringOptions = {
{{"logfile", "File name of the log file to write out to.", "logfile"}, m_logFile},
{{"runpythonargs", "Command-line argument string to pass to the python script if --runpython or --runpythontest was used.", "runpythonargs"}, m_pythonArgs},
{{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythontTestCase},
{{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythonTestCase},
{{"exec", "cfg file to run on startup, used for systems like automation", "exec"}, m_execFile},
{{"rhi", "Command-line argument to force which rhi to use", "dummyString"}, dummyString },
{{"rhi-device-validation", "Command-line argument to configure rhi validation", "dummyString"}, dummyString },
@@ -1535,11 +1535,12 @@ void CCryEditApp::RunInitPythonScript(CEditCommandLineInfo& cmdInfo)
{
// Multiple testcases can be specified them with ';', these should match the files to run
AZStd::vector<AZStd::string_view> testcaseList;
QByteArray pythonTestCase = cmdInfo.m_pythonTestCase.toUtf8();
testcaseList.resize(fileList.size());
{
int i = 0;
AzFramework::StringFunc::TokenizeVisitor(
fileStr.constData(),
pythonTestCase.constData(),
[&i, &testcaseList](AZStd::string_view elem)
{
testcaseList[i++] = (elem);
@@ -75,7 +75,10 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("CaptureCursorLook", &CameraMovementSettings::m_captureCursorLook)
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY);
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY)
->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX)
->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY)
->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ);
serialize.Class<CameraInputSettings>()
->Version(2)
@@ -154,7 +157,16 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
"Invert direction of pan in local Y axis")
->DataElement(
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor",
"Should the cursor be captured (hidden) while performing free look");
"Should the cursor be captured (hidden) while performing free look")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position",
"Default X Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position",
"Default Y Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position",
"Default Z Camera Position when a level is opened");
editContext->Class<CameraInputSettings>("Camera Input Settings", "")
->DataElement(
@@ -271,6 +283,12 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
SandboxEditor::SetDefaultCameraEditorPosition(
AZ::Vector3(
m_cameraMovementSettings.m_defaultCameraPositionX,
m_cameraMovementSettings.m_defaultCameraPositionY,
m_cameraMovementSettings.m_defaultCameraPositionZ
));
SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId);
SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId);
@@ -308,6 +326,11 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition();
m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX();
m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY();
m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ();
m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName();
m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName();
m_cameraInputSettings.m_translateLeftChannelId = SandboxEditor::CameraTranslateLeftChannelId().GetName();
@@ -57,6 +57,9 @@ private:
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
float m_defaultCameraPositionX;
float m_defaultCameraPositionY;
float m_defaultCameraPositionZ;
AZ::Crc32 RotateSmoothingVisibility() const
{
+18
View File
@@ -52,6 +52,9 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraOrbitDollyIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitDollyId";
constexpr AZStd::string_view CameraOrbitPanIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitPanId";
constexpr AZStd::string_view CameraFocusIdSetting = "/Amazon/Preferences/Editor/Camera/FocusId";
constexpr AZStd::string_view CameraDefaultStartingPositionX = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/x";
constexpr AZStd::string_view CameraDefaultStartingPositionY = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/y";
constexpr AZStd::string_view CameraDefaultStartingPositionZ = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/z";
template<typename T>
void SetRegistry(const AZStd::string_view setting, T&& value)
@@ -111,6 +114,21 @@ namespace SandboxEditor
return AZStd::make_unique<EditorViewportSettingsCallbacksImpl>();
}
AZ::Vector3 DefaultEditorCameraPosition()
{
float xPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0));
float yPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0));
float zPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0));
return AZ::Vector3(xPosition, yPosition, zPosition);
}
void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition)
{
SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX());
SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY());
SetRegistry(CameraDefaultStartingPositionZ, defaultCameraPosition.GetZ());
}
AZ::u64 MaxItemsShownInAssetBrowserSearch()
{
return GetRegistry(AssetBrowserMaxItemsShownInSearchSetting, aznumeric_cast<AZ::u64>(50));
+4
View File
@@ -12,6 +12,7 @@
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/Math/Vector3.h>
#include <AzFramework/Input/Channels/InputChannelId.h>
namespace SandboxEditor
@@ -32,6 +33,9 @@ namespace SandboxEditor
//! event will fire when a value in the settings registry (editorpreferences.setreg) is modified.
SANDBOX_API AZStd::unique_ptr<EditorViewportSettingsCallbacks> CreateEditorViewportSettingsCallbacks();
SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition();
SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition);
SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch();
SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown);
+16 -22
View File
@@ -112,7 +112,6 @@ void StartFixedCursorMode(QObject *viewport);
#define RENDER_MESH_TEST_DISTANCE (0.2f)
#define CURSOR_FONT_HEIGHT 8.0f
namespace AZ::ViewportHelpers
{
static const char TextCantCreateCameraNoLevel[] = "Cannot create camera when no level is loaded.";
@@ -623,16 +622,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
PopDisableRendering();
{
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
float sx = terrainAabb.GetXExtent();
float sy = terrainAabb.GetYExtent();
Matrix34 viewTM;
viewTM.SetIdentity();
// Initial camera will be at middle of the map at the height of 2
// meters above the terrain (default terrain height is 32)
viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f));
viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(viewTM);
UpdateScene();
@@ -647,16 +640,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
PopDisableRendering();
{
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
float sx = terrainAabb.GetXExtent();
float sy = terrainAabb.GetYExtent();
Matrix34 viewTM;
viewTM.SetIdentity();
// Initial camera will be at middle of the map at the height of 2
// meters above the terrain (default terrain height is 32)
viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f));
viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(viewTM);
}
break;
@@ -1345,10 +1332,6 @@ void EditorViewportWidget::keyPressEvent(QKeyEvent* event)
void EditorViewportWidget::SetViewTM(const Matrix34& tm)
{
if (m_viewSourceType == ViewSourceType::None)
{
m_defaultViewTM = tm;
}
SetViewTM(tm, false);
}
@@ -1445,6 +1428,10 @@ void EditorViewportWidget::SetViewTM(const Matrix34& camMatrix, bool bMoveOnly)
"Please report this as a bug."
);
}
else if (shouldUpdateObject == ShouldUpdateObject::No)
{
GetCurrentAtomView()->SetCameraTransform(LYTransformToAZMatrix3x4(camMatrix));
}
if (m_pressedKeyState == KeyPressedState::PressedThisFrame)
{
@@ -2028,6 +2015,9 @@ void EditorViewportWidget::SetDefaultCamera()
m_viewSourceType = ViewSourceType::None;
GetViewManager()->SetCameraObjectId(GUID_NULL);
SetName(m_defaultViewName);
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
// Synchronize the configured editor viewport FOV to the default camera
@@ -2530,6 +2520,11 @@ bool EditorViewportSettings::StickySelectEnabled() const
return SandboxEditor::StickySelectEnabled();
}
AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const
{
return SandboxEditor::DefaultEditorCameraPosition();
}
AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once);
bool EditorViewportWidget::ShouldPreviewFullscreen() const
@@ -2641,5 +2636,4 @@ void EditorViewportWidget::StopFullscreenPreview()
// Show the main window
MainWindow::instance()->show();
}
#include <moc_EditorViewportWidget.cpp>
+1
View File
@@ -78,6 +78,7 @@ struct EditorViewportSettings : public AzToolsFramework::ViewportInteraction::Vi
float ManipulatorLineBoundWidth() const override;
float ManipulatorCircleBoundWidth() const override;
bool StickySelectEnabled() const override;
AZ::Vector3 DefaultEditorCameraPosition() const override;
};
// EditorViewportWidget window
@@ -192,11 +192,86 @@ namespace AZ
};
//! SettingsRegistry notifier handler which is responsible for loading
//! the project.json file at the new project path
//! if an update to '<BootstrapSettingsRootKey>/project_path' key occurs.
struct ProjectPathChangedEventHandler
{
ProjectPathChangedEventHandler(AZ::SettingsRegistryInterface& registry)
: m_registry{ registry }
{
}
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
// Update the project settings when the project path is set
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
const auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
AZ::IO::FixedMaxPath newProjectPath;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectPathKey, path)
&& m_registry.Get(newProjectPath.Native(), projectPathKey) && newProjectPath != m_oldProjectPath)
{
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
AZ::IO::FixedMaxPath projectMetadataFile{ AZ::SettingsRegistryMergeUtils::FindEngineRoot(m_registry) / newProjectPath };
projectMetadataFile /= "project.json";
m_registry.MergeSettingsFile(projectMetadataFile.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
}
private:
AZ::IO::FixedMaxPath m_oldProjectPath;
AZ::SettingsRegistryInterface& m_registry;
};
//! SettingsRegistry notifier handler which adds the project name as a specialization tag
//! to the registry
//! if an update to '<ProjectSettingsRootKey>/project_name' key occurs.
struct ProjectNameChangedEventHandler
{
ProjectNameChangedEventHandler(AZ::SettingsRegistryInterface& registry)
: m_registry{ registry }
{
}
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
// Update the project specialization when the project name is set
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
const auto projectNameKey = FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name";
FixedValueString newProjectName;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectNameKey, path)
&& m_registry.Get(newProjectName, projectNameKey) && newProjectName != m_oldProjectName)
{
// Add the project_name as a specialization for loading the build system dependency .setreg files
auto newProjectNameSpecialization = FixedValueString::format("%s/%.*s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
aznumeric_cast<int>(newProjectName.size()), newProjectName.data());
auto oldProjectNameSpecialization = FixedValueString::format("%s/%s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
m_oldProjectName.c_str());
m_registry.Remove(oldProjectNameSpecialization);
m_oldProjectName = newProjectName;
m_registry.Set(newProjectNameSpecialization, true);
}
}
private:
AZ::SettingsRegistryInterface::FixedValueString m_oldProjectName;
AZ::SettingsRegistryInterface& m_registry;
};
//! SettingsRegistry notifier handler which updates relevant registry settings based
//! on an update to '/Amazon/AzCore/Bootstrap/project_path' key.
struct UpdateProjectSettingsEventHandler
struct UpdateCommandLineEventHandler
{
UpdateProjectSettingsEventHandler(AZ::SettingsRegistryInterface& registry, AZ::CommandLine& commandLine)
UpdateCommandLineEventHandler(AZ::SettingsRegistryInterface& registry, AZ::CommandLine& commandLine)
: m_registry{ registry }
, m_commandLine{ commandLine }
{
@@ -204,70 +279,14 @@ namespace AZ
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// #1 Update the project settings when the project path is set
const auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
AZ::IO::FixedMaxPath newProjectPath;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectPathKey, path)
&& m_registry.Get(newProjectPath.Native(), projectPathKey) && newProjectPath != m_oldProjectPath)
{
UpdateProjectSettingsFromProjectPath(AZ::IO::PathView(newProjectPath));
}
// #2 Update the project specialization when the project name is set
const auto projectNameKey = FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name";
FixedValueString newProjectName;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectNameKey, path)
&& m_registry.Get(newProjectName, projectNameKey) && newProjectName != m_oldProjectName)
{
UpdateProjectSpecializationFromProjectName(newProjectName);
}
// #3 Update the ComponentApplication CommandLine instance when the command line settings are merged into the Settings Registry
// Update the ComponentApplication CommandLine instance when the command line settings are merged into the Settings Registry
if (path == AZ::SettingsRegistryMergeUtils::CommandLineValueChangedKey)
{
UpdateCommandLine();
AZ::SettingsRegistryMergeUtils::GetCommandLineFromRegistry(m_registry, m_commandLine);
}
}
//! Add the project name as a specialization underneath the /Amazon/AzCore/Settings/Specializations path
//! and remove the current project name specialization if one exists.
void UpdateProjectSpecializationFromProjectName(AZStd::string_view newProjectName)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// Add the project_name as a specialization for loading the build system dependency .setreg files
auto newProjectNameSpecialization = FixedValueString::format("%s/%.*s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
aznumeric_cast<int>(newProjectName.size()), newProjectName.data());
auto oldProjectNameSpecialization = FixedValueString::format("%s/%s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
m_oldProjectName.c_str());
m_registry.Remove(oldProjectNameSpecialization);
m_oldProjectName = newProjectName;
m_registry.Set(newProjectNameSpecialization, true);
}
void UpdateProjectSettingsFromProjectPath(AZ::IO::PathView newProjectPath)
{
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
AZ::IO::FixedMaxPath projectMetadataFile{ AZ::SettingsRegistryMergeUtils::FindEngineRoot(m_registry) / newProjectPath };
projectMetadataFile /= "project.json";
m_registry.MergeSettingsFile(projectMetadataFile.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
void UpdateCommandLine()
{
AZ::SettingsRegistryMergeUtils::GetCommandLineFromRegistry(m_registry, m_commandLine);
}
private:
AZ::IO::FixedMaxPath m_oldProjectPath;
AZ::SettingsRegistryInterface::FixedValueString m_oldProjectName;
AZ::SettingsRegistryInterface& m_registry;
AZ::CommandLine& m_commandLine;
};
@@ -462,7 +481,12 @@ namespace AZ
// 1. The 'project_path' key changes
// 2. The project specialization when the 'project-name' key changes
// 3. The ComponentApplication command line when the command line is stored to the registry
m_projectChangedHandler = m_settingsRegistry->RegisterNotifier(UpdateProjectSettingsEventHandler{ *m_settingsRegistry, m_commandLine });
m_projectPathChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectPathChangedEventHandler{
*m_settingsRegistry });
m_projectNameChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectNameChangedEventHandler{
*m_settingsRegistry });
m_commandLineUpdatedHandler = m_settingsRegistry->RegisterNotifier(UpdateCommandLineEventHandler{
*m_settingsRegistry, m_commandLine });
// Merge Command Line arguments
constexpr bool executeRegDumpCommands = false;
@@ -515,11 +539,12 @@ namespace AZ
Destroy();
}
// The m_projectChangedHandler stores an AZStd::function internally
// which allocates using the AZ SystemAllocator
// m_projectChangedHandler is being default value initialized
// to clear out the AZStd::function
m_projectChangedHandler = {};
// The SettingsRegistry Notify handlers stores an AZStd::function internally
// which may allocates using the AZ SystemAllocator(if the functor > 16 bytes)
// The handlers are being default value initialized to clear out the AZStd::function
m_commandLineUpdatedHandler = {};
m_projectNameChangedHandler = {};
m_projectPathChangedHandler = {};
// Delete the AZ::IConsole if it was created by this application instance
if (m_ownsConsole)
@@ -390,7 +390,9 @@ namespace AZ
AZ::IO::FixedMaxPath m_engineRoot;
AZ::IO::FixedMaxPath m_appRoot;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectPathChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectNameChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_commandLineUpdatedHandler;
// ConsoleFunctorHandle is responsible for unregistering the Settings Registry Console
// from the m_console member when it goes out of scope
@@ -639,9 +639,10 @@ namespace AZ
{
// Make sure the there is a JSON object at the ConsoleRuntimeCommandKey or ConsoleAutoexecKey
// So that JSON Patch is able to add values underneath that object (JSON Patch doesn't create intermediate objects)
settingsRegistry.MergeSettings(R"({ "Amazon": { "AzCore": { "Runtime": { "ConsoleCommands": {} } } })"
R"(,"O3DE": { "Autoexec": { "ConsoleCommands": {} } } })",
SettingsRegistryInterface::Format::JsonMergePatch);
settingsRegistry.MergeSettings(R"({})", SettingsRegistryInterface::Format::JsonMergePatch,
IConsole::ConsoleRuntimeCommandKey);
settingsRegistry.MergeSettings(R"({})", SettingsRegistryInterface::Format::JsonMergePatch,
IConsole::ConsoleAutoexecCommandKey);
m_consoleCommandKeyHandler = settingsRegistry.RegisterNotifier(ConsoleCommandKeyNotificationHandler{ settingsRegistry, *this });
JsonApplyPatchSettings applyPatchSettings;
@@ -192,26 +192,34 @@ namespace AZ
//! @param path An offset at which traversal should start.
//! @return Whether or not entries could be visited.
virtual bool Visit(const VisitorCallback& callback, AZStd::string_view path) const = 0;
//! Register a callback that will be called whenever an entry gets a new/updated value.
//!
//! @callback The function to call when an entry gets a new/updated value.
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(const NotifyCallback& callback) = 0;
//! Register a callback that will be called whenever an entry gets a new/updated value.
//! @callback The function to call when an entry gets a new/updated value.
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(NotifyCallback&& callback) = 0;
//! @return NotifyEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(NotifyCallback callback) = 0;
//! Register a notify event handler with the NotifyEvent.
//! The handler will be called whenever an entry gets a new/updated value.
//! @param handler The handler to register with the NotifyEvent.
virtual void RegisterNotifier(NotifyEventHandler& handler) = 0;
//! Register a function that will be called before a file is merged.
//! @callback The function to call before a file is merged.
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(const PreMergeEventCallback& callback) = 0;
//! Register a function that will be called before a file is merged.
//! @callback The function to call before a file is merged.
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback&& callback) = 0;
//! @param callback The function to call before a file is merged.
//! @return PreMergeEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback callback) = 0;
//! Register a pre-merge handler with the PreMergeEvent.
//! The handler will be called before a file is merged.
//! @param handler The hanlder to register with the PreMergeEvent.
virtual void RegisterPreMergeEvent(PreMergeEventHandler& handler) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(const PostMergeEventCallback& callback) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback&& callback) = 0;
//! @param callback The function to call after a file is merged.
//! @return PostMergeEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback callback) = 0;
//! Register a post-merge hahndler with the PostMergeEvent.
//! The handler will be called after a file is merged.
//! @param handler The handler to register with the PostmergeEVent.
virtual void RegisterPostMergeEvent(PostMergeEventHandler& hanlder) = 0;
//! Gets the boolean value at the provided path.
//! @param result The target to write the result to.
@@ -326,23 +334,25 @@ namespace AZ
//! - all digits and dot -> floating point number
//! - Everything else is considered a string.
//! @param argument The command line argument.
//! @param structure which contains functors which determine what characters are delimiters
//! @param anchorKey The key where the merged command line argument will be anchored under
//! @param commandLineSettings structure which contains functors which determine what characters are delimiters
//! @return True if the command line argument could be parsed, otherwise false.
virtual bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view rootKey = "",
virtual bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view anchorKey = "",
const CommandLineArgumentSettings& commandLineSettings = {}) = 0;
//! Merges the json data provided into the settings registry.
//! @param data The json data stored in a string.
//! @param format The format of the provided data.
//! @param anchorKey The key where the merged json content will be anchored under.
//! @return True if the data was successfully merged, otherwise false.
virtual bool MergeSettings(AZStd::string_view data, Format format) = 0;
virtual bool MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey = "") = 0;
//! Loads a settings file and merges it into the registry.
//! @param path The path to the registry file.
//! @param format The format of the text data in the file at the provided path.
//! @param rootKey The key where the root of the settings file will be stored under.
//! @param anchorKey The key where the content of the settings file will be anchored.
//! @param scratchBuffer An optional buffer that's used to load the file into. Use this when loading multiple patches to
//! reduce the number of intermediate memory allocations.
//! @return True if the registry file was successfully merged, otherwise false.
virtual bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view rootKey = "",
virtual bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view anchorKey = "",
AZStd::vector<char>* scratchBuffer = nullptr) = 0;
//! Loads all settings files in a folder and merges them into the registry.
//! With the specializations "a" and "b" and platform "c" the files would be loaded in the order:
@@ -357,11 +367,12 @@ namespace AZ
//! @param platform An optional name of a platform. Platform overloads are located at <path>/Platform/<platform>/
//! Files in a platform are applied in the same order as for the main folder but always after the same file
//! in the main folder.
//! @param anchorKey The registry path location where the settings will be anchored
//! @param scratchBuffer An optional buffer that's used to load the file into. Use this when loading multiple patches to
//! reduce the number of intermediate memory allocations.
//! @return True if the registry folder was successfully merged, otherwise false.
virtual bool MergeSettingsFolder(AZStd::string_view path, const Specializations& specializations,
AZStd::string_view platform = {}, AZStd::string_view rootKey = "", AZStd::vector<char>* scratchBuffer = nullptr) = 0;
AZStd::string_view platform = {}, AZStd::string_view anchorKey = "", AZStd::vector<char>* scratchBuffer = nullptr) = 0;
//! Stores the settings structure which is used when merging settings to the Settings Registry
//! using JSON Merge Patch or JSON Merge Patch.
@@ -13,7 +13,7 @@
#include <AzCore/IO/FileReader.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/NativeUI//NativeUIRequests.h>
#include <AzCore/NativeUI/NativeUIRequests.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/StackedString.h>
#include <AzCore/Settings/SettingsRegistryImpl.h>
@@ -21,6 +21,34 @@
#include <AzCore/std/sort.h>
#include <AzCore/std/parallel/scoped_lock.h>
namespace AZ::SettingsRegistryImplInternal
{
AZ::SettingsRegistryInterface::Type RapidjsonToSettingsRegistryType(const rapidjson::Value& value)
{
using Type = AZ::SettingsRegistryInterface::Type;
switch (value.GetType())
{
case rapidjson::Type::kNullType:
return Type::Null;
case rapidjson::Type::kFalseType:
return Type::Boolean;
case rapidjson::Type::kTrueType:
return Type::Boolean;
case rapidjson::Type::kObjectType:
return Type::Object;
case rapidjson::Type::kArrayType:
return Type::Array;
case rapidjson::Type::kStringType:
return Type::String;
case rapidjson::Type::kNumberType:
return value.IsDouble() ? Type::FloatingPoint :
Type::Integer;
}
return Type::NoType;
}
}
namespace AZ
{
template<typename T>
@@ -28,7 +56,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -70,7 +98,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -161,7 +189,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -212,17 +240,7 @@ namespace AZ
return Visit(visitor, path);
}
auto SettingsRegistryImpl::RegisterNotifier(const NotifyCallback& callback) -> NotifyEventHandler
{
NotifyEventHandler notifyHandler{ callback };
{
AZStd::scoped_lock lock(m_notifierMutex);
notifyHandler.Connect(m_notifiers);
}
return notifyHandler;
}
auto SettingsRegistryImpl::RegisterNotifier(NotifyCallback&& callback) -> NotifyEventHandler
auto SettingsRegistryImpl::RegisterNotifier(NotifyCallback callback) -> NotifyEventHandler
{
NotifyEventHandler notifyHandler{ AZStd::move(callback) };
{
@@ -232,23 +250,19 @@ namespace AZ
return notifyHandler;
}
auto SettingsRegistryImpl::RegisterNotifier(NotifyEventHandler& notifyHandler) -> void
{
AZStd::scoped_lock lock(m_notifierMutex);
notifyHandler.Connect(m_notifiers);
}
void SettingsRegistryImpl::ClearNotifiers()
{
AZStd::scoped_lock lock(m_notifierMutex);
m_notifiers.DisconnectAllHandlers();
}
auto SettingsRegistryImpl::RegisterPreMergeEvent(const PreMergeEventCallback& callback) -> PreMergeEventHandler
{
PreMergeEventHandler preMergeHandler{ callback };
{
AZStd::scoped_lock lock(m_settingMutex);
preMergeHandler.Connect(m_preMergeEvent);
}
return preMergeHandler;
}
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventCallback&& callback) -> PreMergeEventHandler
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventCallback callback) -> PreMergeEventHandler
{
PreMergeEventHandler preMergeHandler{ AZStd::move(callback) };
{
@@ -258,17 +272,13 @@ namespace AZ
return preMergeHandler;
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(const PostMergeEventCallback& callback) -> PostMergeEventHandler
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventHandler& preMergeHandler) -> void
{
PostMergeEventHandler postMergeHandler{ callback };
{
AZStd::scoped_lock lock(m_settingMutex);
postMergeHandler.Connect(m_postMergeEvent);
}
return postMergeHandler;
AZStd::scoped_lock lock(m_settingMutex);
preMergeHandler.Connect(m_preMergeEvent);
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventCallback&& callback) -> PostMergeEventHandler
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventCallback callback) -> PostMergeEventHandler
{
PostMergeEventHandler postMergeHandler{ AZStd::move(callback) };
{
@@ -278,6 +288,12 @@ namespace AZ
return postMergeHandler;
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventHandler& postMergeHandler) -> void
{
AZStd::scoped_lock lock(m_settingMutex);
postMergeHandler.Connect(m_postMergeEvent);
}
void SettingsRegistryImpl::ClearMergeEvents()
{
AZStd::scoped_lock lock(m_settingMutex);
@@ -297,7 +313,36 @@ namespace AZ
localNotifierEvent = AZStd::move(m_notifiers);
}
localNotifierEvent.Signal(jsonPath, type);
// Signal the NotifyEvent for each queued argument
decltype(m_signalNotifierQueue) localNotifierQueue;
{
AZStd::scoped_lock signalLock(m_signalMutex);
m_signalNotifierQueue.push_back({ FixedValueString{jsonPath}, type });
// If the signal count was 0, then a dispatch is in progress
if (m_signalCount++ == 0)
{
AZStd::swap(localNotifierQueue, m_signalNotifierQueue);
}
}
while (!localNotifierQueue.empty())
{
for (SignalNotifierArgs notifierArgs : localNotifierQueue)
{
localNotifierEvent.Signal(notifierArgs.m_jsonPath, notifierArgs.m_type);
}
// Clear the local notifier queue and check if more notifiers have been added
localNotifierQueue = {};
{
AZStd::scoped_lock signalLock(m_signalMutex);
AZStd::swap(localNotifierQueue, m_signalNotifierQueue);
}
}
{
AZStd::scoped_lock signalLock(m_signalMutex);
--m_signalCount;
}
{
// Swap the local handlers with the current m_notifiers which
@@ -314,39 +359,19 @@ namespace AZ
{
if (path.empty())
{
//rapidjson::Pointer assets that the supplied string
//rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
}
rapidjson::Pointer pointer(path.data(), path.length());
if (pointer.IsValid())
{
AZStd::scoped_lock lock(m_settingMutex);
const rapidjson::Value* value = pointer.Get(m_settings);
if (value)
if (const rapidjson::Value* value = pointer.Get(m_settings); value != nullptr)
{
switch (value->GetType())
{
case rapidjson::Type::kNullType:
return Type::Null;
case rapidjson::Type::kFalseType:
return Type::Boolean;
case rapidjson::Type::kTrueType:
return Type::Boolean;
case rapidjson::Type::kObjectType:
return Type::Object;
case rapidjson::Type::kArrayType:
return Type::Array;
case rapidjson::Type::kStringType:
return Type::String;
case rapidjson::Type::kNumberType:
return
value->IsDouble() ? Type::FloatingPoint :
Type::Integer;
}
return SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(*value);
}
}
return Type::NoType;
@@ -392,7 +417,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -471,13 +496,12 @@ namespace AZ
{
if (path.empty())
{
//rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
}
rapidjson::Pointer pointer(path.data(), path.length());
if (pointer.IsValid())
{
@@ -486,10 +510,14 @@ namespace AZ
value, nullptr, valueTypeID, m_serializationSettings);
if (jsonResult.GetProcessing() != JsonSerializationResult::Processing::Halted)
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& setting = pointer.Create(m_settings, m_settings.GetAllocator());
setting = AZStd::move(store);
SignalNotifier(path, Type::Object);
auto anchorType = Type::NoType;
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& setting = pointer.Create(m_settings, m_settings.GetAllocator());
setting = AZStd::move(store);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(setting);
}
SignalNotifier(path, anchorType);
return true;
}
}
@@ -500,7 +528,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -605,7 +633,7 @@ namespace AZ
return Set(key, value);
}
bool SettingsRegistryImpl::MergeSettings(AZStd::string_view data, Format format)
bool SettingsRegistryImpl::MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey)
{
rapidjson::Document jsonPatch;
constexpr int flags = rapidjson::kParseStopWhenDoneFlag | rapidjson::kParseCommentsFlag | rapidjson::kParseTrailingCommasFlag;
@@ -631,17 +659,43 @@ namespace AZ
return false;
}
AZStd::scoped_lock lock(m_settingMutex);
JsonSerializationResult::ResultCode mergeResult =
JsonSerialization::ApplyPatch(m_settings, m_settings.GetAllocator(), jsonPatch, mergeApproach);
if (mergeResult.GetProcessing() != JsonSerializationResult::Processing::Completed)
rapidjson::Pointer anchorPath;
if (!anchorKey.empty())
{
AZ_Error("Settings Registry", false, "Failed to fully merge data into registry.");
return false;
anchorPath = rapidjson::Pointer(anchorKey.data(), anchorKey.size());
if (!anchorPath.IsValid())
{
rapidjson::Pointer pointer(AZ_SETTINGS_REGISTRY_HISTORY_KEY "/-");
AZ_Error("Settings Registry", false, R"(Anchor path "%.*s" is invalid.)", AZ_STRING_ARG(anchorKey));
AZStd::scoped_lock lock(m_settingMutex);
pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
.AddMember(rapidjson::StringRef("Error"), rapidjson::StringRef("Invalid anchor key."), m_settings.GetAllocator())
.AddMember(rapidjson::StringRef("Path"),
rapidjson::Value(anchorKey.data(), aznumeric_caster(anchorKey.size()), m_settings.GetAllocator()),
m_settings.GetAllocator());
return false;
}
}
SignalNotifier("", Type::Object);
auto anchorType = AZ::SettingsRegistryInterface::Type::NoType;
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& anchorRoot = anchorPath.IsValid() ? anchorPath.Create(m_settings, m_settings.GetAllocator())
: m_settings;
JsonSerializationResult::ResultCode mergeResult =
JsonSerialization::ApplyPatch(anchorRoot, m_settings.GetAllocator(), jsonPatch, mergeApproach);
if (mergeResult.GetProcessing() != JsonSerializationResult::Processing::Completed)
{
AZ_Error("Settings Registry", false, "Failed to fully merge data into registry.");
return false;
}
// The settings have been successfully merged, query the type at the anchor key
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(anchorRoot);
}
SignalNotifier(anchorKey, anchorType);
return true;
}
@@ -1225,10 +1279,12 @@ namespace AZ
ScopedMergeEvent scopedMergeEvent(m_preMergeEvent, m_postMergeEvent, path, rootKey);
JsonSerializationResult::ResultCode mergeResult(JsonSerializationResult::Tasks::Merge);
auto anchorType = Type::NoType;
if (rootKey.empty())
{
AZStd::scoped_lock lock(m_settingMutex);
mergeResult = JsonSerialization::ApplyPatch(m_settings, m_settings.GetAllocator(), jsonPatch, mergeApproach, m_applyPatchSettings);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(m_settings);
}
else
{
@@ -1238,6 +1294,7 @@ namespace AZ
AZStd::scoped_lock lock(m_settingMutex);
Value& rootValue = root.Create(m_settings, m_settings.GetAllocator());
mergeResult = JsonSerialization::ApplyPatch(rootValue, m_settings.GetAllocator(), jsonPatch, mergeApproach, m_applyPatchSettings);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(rootValue);
}
else
{
@@ -1265,7 +1322,7 @@ namespace AZ
pointer.Create(m_settings, m_settings.GetAllocator()).SetString(path, m_settings.GetAllocator());
}
SignalNotifier("", Type::Object);
SignalNotifier(rootKey, anchorType);
return true;
}
@@ -48,14 +48,14 @@ namespace AZ
Type GetType(AZStd::string_view path) const override;
bool Visit(Visitor& visitor, AZStd::string_view path) const override;
bool Visit(const VisitorCallback& callback, AZStd::string_view path) const override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(const NotifyCallback& callback) override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(NotifyCallback&& callback) override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(NotifyCallback callback) override;
void RegisterNotifier(NotifyEventHandler& hanlder) override;
void ClearNotifiers();
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(const PreMergeEventCallback& callback) override;
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback&& callback) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(const PostMergeEventCallback& callback) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback&& callback) override;
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback callback) override;
void RegisterPreMergeEvent(PreMergeEventHandler& handler) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback callback) override;
void RegisterPostMergeEvent(PostMergeEventHandler& handler) override;
void ClearMergeEvents();
bool Get(bool& result, AZStd::string_view path) const override;
@@ -76,13 +76,13 @@ namespace AZ
bool Remove(AZStd::string_view path) override;
bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view rootKey,
bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view anchorKey,
const CommandLineArgumentSettings& commandLineSettings) override;
bool MergeSettings(AZStd::string_view data, Format format) override;
bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view rootKey,
bool MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey = "") override;
bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view anchorKey = "",
AZStd::vector<char>* scratchBuffer = nullptr) override;
bool MergeSettingsFolder(AZStd::string_view path, const Specializations& specializations,
AZStd::string_view platform, AZStd::string_view rootKey = "", AZStd::vector<char>* scratchBuffer = nullptr) override;
AZStd::string_view platform, AZStd::string_view anchorKey = "", AZStd::vector<char>* scratchBuffer = nullptr) override;
void SetApplyPatchSettings(const AZ::JsonApplyPatchSettings& applyPatchSettings) override;
void GetApplyPatchSettings(AZ::JsonApplyPatchSettings& applyPatchSettings) override;
@@ -121,6 +121,19 @@ namespace AZ
PreMergeEvent m_preMergeEvent;
PostMergeEvent m_postMergeEvent;
//! NOTE: During SignalNotifier, the registered notify event handlers are moved to a local NotifyEvent
//! Therefore setting a value within the registry during signaling will queue future SignalNotifer calls
//! These calls will then be invoked after the current signaling has completex
//! This is done to avoid deadlock if another thread attempts to access register a notifier or signal one
mutable AZStd::mutex m_signalMutex;
struct SignalNotifierArgs
{
FixedValueString m_jsonPath;
Type m_type;
};
AZStd::deque<SignalNotifierArgs> m_signalNotifierQueue;
AZStd::atomic_int m_signalCount{};
rapidjson::Document m_settings;
JsonSerializerSettings m_serializationSettings;
JsonDeserializerSettings m_deserializationSettings;
@@ -23,12 +23,12 @@ namespace AZ
MOCK_CONST_METHOD1(GetType, Type(AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool(Visitor&, AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool(const VisitorCallback&, AZStd::string_view));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(const NotifyCallback&));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(NotifyCallback&&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(const PreMergeEventCallback&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(PreMergeEventCallback&&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(const PostMergeEventCallback&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(PostMergeEventCallback&&));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(NotifyCallback));
MOCK_METHOD1(RegisterNotifier, void(NotifyEventHandler&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(PreMergeEventCallback));
MOCK_METHOD1(RegisterPreMergeEvent, void(PreMergeEventHandler&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(PostMergeEventCallback));
MOCK_METHOD1(RegisterPostMergeEvent, void(PostMergeEventHandler&));
MOCK_CONST_METHOD2(Get, bool(bool&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool(s64&, AZStd::string_view));
@@ -49,7 +49,7 @@ namespace AZ
MOCK_METHOD1(Remove, bool(AZStd::string_view));
MOCK_METHOD3(MergeCommandLineArgument, bool(AZStd::string_view, AZStd::string_view, const CommandLineArgumentSettings&));
MOCK_METHOD2(MergeSettings, bool(AZStd::string_view, Format));
MOCK_METHOD3(MergeSettings, bool(AZStd::string_view, Format, AZStd::string_view));
MOCK_METHOD4(MergeSettingsFile, bool(AZStd::string_view, Format, AZStd::string_view, AZStd::vector<char>*));
MOCK_METHOD5(
MergeSettingsFolder,
@@ -1299,6 +1299,35 @@ namespace SettingsRegistryTests
EXPECT_FALSE(m_registry->MergeCommandLineArgument(" ", {}, {}));
}
//
// MergeSettings
//
TEST_F(SettingsRegistryTest, MergeSettings_MergeJsonWithAnchorKey_StoresSettingsUnderneathKey)
{
constexpr AZStd::string_view anchorKey = "/Anchor/Root/0";
constexpr auto mergeFormat = AZ::SettingsRegistryInterface::Format::JsonMergePatch;
EXPECT_TRUE(m_registry->MergeSettings(R"({ "Test": "1" })", mergeFormat, anchorKey));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Array, m_registry->GetType("/Anchor/Root"));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Object, m_registry->GetType("/Anchor/Root/0"));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::String, m_registry->GetType("/Anchor/Root/0/Test"));
}
TEST_F(SettingsRegistryTest, MergeSettings_NotifierSignals_AtAnchorKeyAndStoresMergeType)
{
AZStd::string_view anchorKey = "/Anchor/Root";
bool callbackInvoked{};
auto callback = [anchorKey, &callbackInvoked](AZStd::string_view path, AZ::SettingsRegistryInterface::Type type)
{
EXPECT_EQ(anchorKey, path);
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Array, type);
callbackInvoked = true;
};
auto testNotifier1 = m_registry->RegisterNotifier(callback);
constexpr auto mergeFormat = AZ::SettingsRegistryInterface::Format::JsonMergePatch;
EXPECT_TRUE(m_registry->MergeSettings(R"([ "Test" ])", mergeFormat, anchorKey));
EXPECT_TRUE(callbackInvoked);
}
//
// MergeSettingsFile
//
@@ -1331,7 +1360,7 @@ namespace SettingsRegistryTests
auto callback = [this](AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
EXPECT_TRUE(path.empty());
EXPECT_EQ("/Path", path);
AZ::s64 value = -1;
bool result = m_registry->Get(value, "/Path/Test");
EXPECT_TRUE(result);
@@ -1924,13 +1924,11 @@ namespace AZ::IO
ArchiveLocationPriority Archive::GetPakPriority() const
{
int pakPriority = aznumeric_cast<int>(ArchiveVars{}.nPriority);
#if defined(AZ_ENABLE_TRACING)
if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
{
AZ::GetValueResult getCvarResult = console->GetCvarValue("sys_PakPriority", pakPriority);
[[maybe_unused]] AZ::GetValueResult getCvarResult = console->GetCvarValue("sys_PakPriority", pakPriority);
AZ_Error("Archive", getCvarResult == AZ::GetValueResult::Success, "Lookup of 'sys_PakPriority console variable failed with error %s", AZ::GetEnumString(getCvarResult));
}
#endif
return static_cast<ArchiveLocationPriority>(pakPriority);
}
@@ -30,22 +30,42 @@ namespace AzFramework
//////////////////////////////////////////////////////////////////////////
// OnSessionHealthCheck is fired in health check process
// @return The result of all OnSessionHealthCheck
// Use this notification to perform any custom health check
// @return True if OnSessionHealthCheck succeeds, false otherwise
virtual bool OnSessionHealthCheck() = 0;
// OnCreateSessionBegin is fired at the beginning of session creation
// OnCreateSessionBegin is fired at the beginning of session creation process
// Use this notification to perform any necessary configuration or initialization before
// creating session
// @param sessionConfig The properties to describe a session
// @return The result of all OnCreateSessionBegin notifications
// @return True if OnCreateSessionBegin succeeds, false otherwise
virtual bool OnCreateSessionBegin(const SessionConfig& sessionConfig) = 0;
// OnDestroySessionBegin is fired at the beginning of session termination
// @return The result of all OnDestroySessionBegin notifications
// OnCreateSessionEnd is fired at the end of session creation process
// Use this notification to perform any follow-up operation after session is created and active
virtual void OnCreateSessionEnd() = 0;
// OnDestroySessionBegin is fired at the beginning of session termination process
// Use this notification to perform any cleanup operation before destroying session,
// like gracefully disconnect players, cleanup data, etc.
// @return True if OnDestroySessionBegin succeeds, false otherwise
virtual bool OnDestroySessionBegin() = 0;
// OnUpdateSessionBegin is fired at the beginning of session update
// OnDestroySessionEnd is fired at the end of session termination process
// Use this notification to perform any follow-up operation after session is destroyed,
// like shutdown application process, etc.
virtual void OnDestroySessionEnd() = 0;
// OnUpdateSessionBegin is fired at the beginning of session update process
// Use this notification to perform any configuration or initialization to handle
// the session settings changing
// @param sessionConfig The properties to describe a session
// @param updateReason The reason for session update
virtual void OnUpdateSessionBegin(const SessionConfig& sessionConfig, const AZStd::string& updateReason) = 0;
// OnUpdateSessionBegin is fired at the end of session update process
// Use this notification to perform any follow-up operations after session is updated
virtual void OnUpdateSessionEnd() = 0;
};
using SessionNotificationBus = AZ::EBus<SessionNotifications>;
} // namespace AzFramework
@@ -46,6 +46,8 @@ namespace AzManipulatorTestFramework
virtual void UpdateVisibility() = 0;
//! Set if sticky select is enabled or not.
virtual void SetStickySelect(bool enabled) = 0;
//! Get default Editor Camera Position.
virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0;
};
//! This interface is used to simulate the manipulator manager while the manipulators are under test.
@@ -36,6 +36,7 @@ namespace AzManipulatorTestFramework
int GetViewportId() const override;
void UpdateVisibility() override;
void SetStickySelect(bool enabled) override;
AZ::Vector3 DefaultEditorCameraPosition() const override;
// ViewportInteractionRequestBus overrides ...
AzFramework::CameraState GetCameraState() override;
@@ -120,6 +120,11 @@ namespace AzManipulatorTestFramework
m_stickySelect = enabled;
}
AZ::Vector3 ViewportInteraction::DefaultEditorCameraPosition() const
{
return {};
}
void ViewportInteraction::SetGridSize(float size)
{
m_gridSize = size;
@@ -65,7 +65,7 @@ namespace AzNetworking
: m_delta(delta)
, m_dataSerializer(m_delta.GetBufferPtr(), m_delta.GetBufferCapacity())
{
m_namePrefix.reserve(128);
;
}
DeltaSerializerCreate::~DeltaSerializerCreate()
@@ -73,7 +73,7 @@ namespace AzNetworking
// Delete any left over records that might be hanging around
for (auto iter : m_records)
{
delete iter.second;
delete iter;
}
m_records.clear();
}
@@ -160,28 +160,13 @@ namespace AzNetworking
return SerializeHelper(buffer, bufferCapacity, isString, outSize, name);
}
AZStd::string DeltaSerializerCreate::GetNextObjectName(const char* name)
bool DeltaSerializerCreate::BeginObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName)
{
AZStd::string objectName = name;
objectName += ".";
objectName += AZStd::to_string(m_objectCounter);
++m_objectCounter;
return objectName;
}
bool DeltaSerializerCreate::BeginObject(const char* name, [[maybe_unused]] const char* typeName)
{
m_nameLengthStack.push_back(m_namePrefix.length());
m_namePrefix += GetNextObjectName(name);
m_namePrefix += ".";
return true;
}
bool DeltaSerializerCreate::EndObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName)
{
const size_t prevLen = m_nameLengthStack.back();
m_nameLengthStack.pop_back();
m_namePrefix.resize(prevLen);
return true;
}
@@ -205,25 +190,15 @@ namespace AzNetworking
{
typedef AbstractValue::ValueT<T> ValueType;
const size_t prevLen = m_namePrefix.length();
m_namePrefix += GetNextObjectName(name);
const AZ::HashValue32 nameHash = AZ::TypeHash32(m_namePrefix.c_str());
m_namePrefix.resize(prevLen);
AbstractValue::BaseValue*& baseValue = m_records[nameHash];
AbstractValue::BaseValue* baseValue = m_records.size() > m_objectCounter ? m_records[m_objectCounter] : nullptr;
++m_objectCounter;
// If we are in the gather records phase, just save off the value records
if (m_gatheringRecords)
{
if (baseValue != nullptr)
{
AZ_Assert(false, "Duplicate name encountered in delta serializer. This will cause data to be serialized incorrectly.");
return false;
}
AZ_Assert(baseValue == nullptr, "Expected to create a new record but found a pre-existing one at index %d", m_objectCounter - 1);
baseValue = new ValueType(value);
m_records.push_back(baseValue);
}
else // If we are not gathering records, then we are comparing them
{
@@ -90,8 +90,6 @@ namespace AzNetworking
DeltaSerializerCreate(const DeltaSerializerCreate&) = delete;
DeltaSerializerCreate& operator=(const DeltaSerializerCreate&) = delete;
AZStd::string GetNextObjectName(const char* name);
template <typename T>
bool SerializeHelper(T& value, uint32_t bufferCapacity, bool isString, uint32_t& outSize, const char* name);
@@ -105,9 +103,7 @@ namespace AzNetworking
bool m_gatheringRecords = false;
uint32_t m_objectCounter = 0;
AZStd::string m_namePrefix;
AZStd::vector<size_t> m_nameLengthStack;
AZStd::unordered_map<AZ::HashValue32, AbstractValue::BaseValue*> m_records;
AZStd::vector<AbstractValue::BaseValue*> m_records;
NetworkInputSerializer m_dataSerializer;
};
@@ -11,4 +11,213 @@
namespace UnitTest
{
struct DeltaDataElement
{
AzNetworking::PacketId m_packetId = AzNetworking::InvalidPacketId;
uint32_t m_id = 0;
AZ::TimeMs m_timeMs = AZ::TimeMs{ 0 };
float m_blendFactor = 0.f;
AZStd::vector<int> m_growVector, m_shrinkVector;
bool Serialize(AzNetworking::ISerializer& serializer)
{
if (!serializer.Serialize(m_packetId, "PacketId")
|| !serializer.Serialize(m_id, "Id")
|| !serializer.Serialize(m_timeMs, "TimeMs")
|| !serializer.Serialize(m_blendFactor, "BlendFactor")
|| !serializer.Serialize(m_growVector, "GrowVector")
|| !serializer.Serialize(m_shrinkVector, "ShrinkVector"))
{
return false;
}
return true;
}
};
struct DeltaDataContainer
{
AZStd::string m_containerName;
AZStd::array<DeltaDataElement, 32> m_container;
// This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem
bool Serialize(AzNetworking::ISerializer& serializer)
{
// Always serialize the full first element
if(!m_container[0].Serialize(serializer))
{
return false;
}
for (uint32_t i = 1; i < m_container.size(); ++i)
{
if (serializer.GetSerializerMode() == AzNetworking::SerializerMode::WriteToObject)
{
AzNetworking::SerializerDelta deltaSerializer;
// Read out the delta
if (!deltaSerializer.Serialize(serializer))
{
return false;
}
// Start with previous value
m_container[i] = m_container[i - 1];
// Then apply delta
AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer);
if (!applySerializer.ApplyDelta(m_container[i]))
{
return false;
}
}
else
{
AzNetworking::SerializerDelta deltaSerializer;
// Create the delta
AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer);
if (!createSerializer.CreateDelta(m_container[i - 1], m_container[i]))
{
return false;
}
// Then write out the delta
if (!deltaSerializer.Serialize(serializer))
{
return false;
}
}
}
return true;
}
// This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem
bool SerializeNoDelta(AzNetworking::ISerializer& serializer)
{
for (uint32_t i = 0; i < m_container.size(); ++i)
{
if(!m_container[i].Serialize(serializer))
{
return false;
}
}
return true;
}
};
class DeltaSerializerTests
: public UnitTest::AllocatorsTestFixture
{
public:
void SetUp() override
{
UnitTest::AllocatorsTestFixture::SetUp();
}
void TearDown() override
{
UnitTest::AllocatorsTestFixture::TearDown();
}
};
static constexpr float BLEND_FACTOR_SCALE = 1.1f;
static constexpr uint32_t TIME_SCALE = 10;
DeltaDataContainer TestDeltaContainer()
{
DeltaDataContainer testContainer;
AZStd::vector<int> growVector, shrinkVector;
shrinkVector.resize(testContainer.m_container.array_size);
testContainer.m_containerName = "TestContainer";
for (int i = 0; i < testContainer.m_container.array_size; ++i)
{
testContainer.m_container[i].m_packetId = AzNetworking::PacketId(i);
testContainer.m_container[i].m_id = i;
testContainer.m_container[i].m_timeMs = AZ::TimeMs(i * TIME_SCALE);
testContainer.m_container[i].m_blendFactor = BLEND_FACTOR_SCALE * i;
growVector.push_back(i);
testContainer.m_container[i].m_growVector = growVector;
shrinkVector.resize(testContainer.m_container.array_size - i);
testContainer.m_container[i].m_shrinkVector = shrinkVector;
}
return testContainer;
}
TEST_F(DeltaSerializerTests, DeltaArray)
{
DeltaDataContainer inContainer = TestDeltaContainer();
AZStd::array<uint8_t, 2048> buffer;
AzNetworking::NetworkInputSerializer inSerializer(buffer.data(), static_cast<uint32_t>(buffer.size()));
// Always serialize the full first element
EXPECT_TRUE(inContainer.Serialize(inSerializer));
DeltaDataContainer outContainer;
AzNetworking::NetworkOutputSerializer outSerializer(buffer.data(), static_cast<uint32_t>(buffer.size()));
EXPECT_TRUE(outContainer.Serialize(outSerializer));
for (uint32_t i = 0; i > outContainer.m_container.size(); ++i)
{
EXPECT_EQ(inContainer.m_container[i].m_blendFactor, outContainer.m_container[i].m_blendFactor);
EXPECT_EQ(inContainer.m_container[i].m_id, outContainer.m_container[i].m_id);
EXPECT_EQ(inContainer.m_container[i].m_packetId, outContainer.m_container[i].m_packetId);
EXPECT_EQ(inContainer.m_container[i].m_timeMs, outContainer.m_container[i].m_timeMs);
EXPECT_EQ(inContainer.m_container[i].m_growVector[i], outContainer.m_container[i].m_growVector[i]);
EXPECT_EQ(inContainer.m_container[i].m_growVector.size(), outContainer.m_container[i].m_growVector.size());
EXPECT_EQ(inContainer.m_container[i].m_shrinkVector.size(), outContainer.m_container[i].m_shrinkVector.size());
}
}
TEST_F(DeltaSerializerTests, DeltaSerializerCreateUnused)
{
// Every function here should return a constant value regardless of inputs
AzNetworking::SerializerDelta deltaSerializer;
AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer);
EXPECT_EQ(createSerializer.GetCapacity(), 0);
EXPECT_EQ(createSerializer.GetSize(), 0);
EXPECT_EQ(createSerializer.GetBuffer(), nullptr);
EXPECT_EQ(createSerializer.GetSerializerMode(), AzNetworking::SerializerMode::ReadFromObject);
createSerializer.ClearTrackedChangesFlag(); //NO-OP
EXPECT_FALSE(createSerializer.GetTrackedChangesFlag());
EXPECT_TRUE(createSerializer.BeginObject("CreateSerializer", "Begin"));
EXPECT_TRUE(createSerializer.EndObject("CreateSerializer", "End"));
}
TEST_F(DeltaSerializerTests, DeltaArraySize)
{
DeltaDataContainer deltaContainer = TestDeltaContainer();
DeltaDataContainer noDeltaContainer = TestDeltaContainer();
AZStd::array<uint8_t, 2048> deltaBuffer;
AzNetworking::NetworkInputSerializer deltaSerializer(deltaBuffer.data(), static_cast<uint32_t>(deltaBuffer.size()));
AZStd::array<uint8_t, 2048> noDeltaBuffer;
AzNetworking::NetworkInputSerializer noDeltaSerializer(noDeltaBuffer.data(), static_cast<uint32_t>(noDeltaBuffer.size()));
EXPECT_TRUE(deltaContainer.Serialize(deltaSerializer));
EXPECT_FALSE(noDeltaContainer.SerializeNoDelta(noDeltaSerializer)); // Should run out of space
EXPECT_EQ(noDeltaSerializer.GetCapacity(), noDeltaSerializer.GetSize()); // Verify that the serializer filled up
EXPECT_FALSE(noDeltaSerializer.IsValid()); // and that it is no longer valid due to lack of space
}
TEST_F(DeltaSerializerTests, DeltaSerializerApplyUnused)
{
// Every function here should return a constant value regardless of inputs
AzNetworking::SerializerDelta deltaSerializer;
AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer);
EXPECT_EQ(applySerializer.GetCapacity(), 0);
EXPECT_EQ(applySerializer.GetSize(), 0);
EXPECT_EQ(applySerializer.GetBuffer(), nullptr);
EXPECT_EQ(applySerializer.GetSerializerMode(), AzNetworking::SerializerMode::WriteToObject);
applySerializer.ClearTrackedChangesFlag(); //NO-OP
EXPECT_FALSE(applySerializer.GetTrackedChangesFlag());
EXPECT_TRUE(applySerializer.BeginObject("CreateSerializer", "Begin"));
EXPECT_TRUE(applySerializer.EndObject("CreateSerializer", "End"));
}
}
@@ -381,22 +381,13 @@ namespace AzToolsFramework
return;
}
bool isPrefabSystemEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
isPrefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled);
// For slices, orphan any children that remain attached to the entity
// For prefabs, this is an unneeded operation because the prefab system handles the orphans
// and the extra reparenting operation can be problematic for consumers subscribed to entity
// events, such as the entity outliner.
if (!isPrefabSystemEnabled)
// Even though these child entities will immediately be destroyed, their entity info may be recycled
// Ensure they don't have any lingering inaccurate parent data
auto children = entityInfo.GetChildren();
for (auto childId : children)
{
auto children = entityInfo.GetChildren();
for (auto childId : children)
{
ReparentChild(childId, AZ::EntityId(), entityId);
m_entityOrphanTable[entityId].insert(childId);
}
ReparentChild(childId, AZ::EntityId(), entityId);
m_entityOrphanTable[entityId].insert(childId);
}
m_savedOrderInfo[entityId] = AZStd::make_pair(entityInfo.GetParent(), entityInfo.GetIndexForSorting());
@@ -1200,26 +1191,41 @@ namespace AzToolsFramework
auto childItr = m_childIndexCache.find(childId);
if (childItr == m_childIndexCache.end())
{
//cache indices for faster lookup
m_childIndexCache[childId] = static_cast<AZ::u64>(m_children.size());
m_children.push_back(childId);
// m_children is guaranteed to be ordered by EntityId, do a sorted insertion
auto insertedChildIndex = AZStd::upper_bound(m_children.begin(), m_children.end(), childId);
insertedChildIndex = m_children.insert(insertedChildIndex, childId);
// Cache all affected child indices for fast lookup
for (auto it = insertedChildIndex; it != m_children.end(); ++it)
{
const AZ::u64 newChildIndex = static_cast<AZ::u64>(it - m_children.begin());
m_childIndexCache[*it] = newChildIndex;
}
}
}
void EditorEntityModel::EditorEntityModelEntry::RemoveChild(AZ::EntityId childId)
{
auto childItr = m_childIndexCache.find(childId);
if (childItr != m_childIndexCache.end())
// Retrieve our child index from the cache
auto cachedIndexItr = m_childIndexCache.find(childId);
if (cachedIndexItr == m_childIndexCache.end())
{
// Take the last entry and move it into the removed spot instead of deleting the entry and having to move all
// following entries one step down.
AZ::EntityId backEntity = m_children.back();
m_children[childItr->second] = backEntity;
// Update cached index for the moved id to the new index.
m_childIndexCache[backEntity] = childItr->second;
// Now remove the deleted id from the children and cache.
m_childIndexCache.erase(childId);
m_children.erase(m_children.end() - 1);
AZ_Assert(false, "Attempted to remove an unknown child");
return;
}
// Build an iterator for m_children based on our cached index
auto childItr = m_children.begin() + cachedIndexItr->second;
// Remove our child from the cache
m_childIndexCache.erase(cachedIndexItr);
// Remove our child, fix up the cache entries for any subsequent children
auto elementsToFixItr = m_children.erase(childItr);
for (auto it = elementsToFixItr; it != m_children.end(); ++it)
{
const AZ::u64 newChildIndex = static_cast<AZ::u64>(it - m_children.begin());
m_childIndexCache[*it] = newChildIndex;
}
}
@@ -1256,8 +1262,17 @@ namespace AzToolsFramework
AZ::u64 EditorEntityModel::EditorEntityModelEntry::GetChildIndex(AZ::EntityId childId) const
{
// Return the cached index, if available.
auto childItr = m_childIndexCache.find(childId);
return childItr != m_childIndexCache.end() ? childItr->second : static_cast<AZ::u64>(m_children.size());
if (childItr != m_childIndexCache.end())
{
return childItr->second;
}
// On initialization, GetChildIndex may be queried for a childId that is not yet in the child list.
// Return the position it would be inserted at in EditorEntityModelEntry::AddChild
auto targetChildPositionItr = AZStd::upper_bound(m_children.begin(), m_children.end(), childId);
return static_cast<AZ::u64>(targetChildPositionItr - m_children.begin());
}
AZStd::string EditorEntityModel::EditorEntityModelEntry::GetName() const
@@ -119,6 +119,12 @@ namespace AzToolsFramework
int EntityOutlinerListModel::rowCount(const QModelIndex& parent) const
{
// For QTreeView models, non-0 columns shouldn't have children
if (parent.isValid() && parent.column() != 0)
{
return 0;
}
auto parentId = GetEntityFromIndex(parent);
AZStd::size_t childCount = 0;
@@ -133,17 +139,13 @@ namespace AzToolsFramework
QModelIndex EntityOutlinerListModel::index(int row, int column, const QModelIndex& parent) const
{
// sanity check
if (!hasIndex(row, column, parent) || (parent.isValid() && parent.column() != 0) || (row < 0 || row >= rowCount(parent)))
{
return QModelIndex();
}
auto parentId = GetEntityFromIndex(parent);
// We have the row and column, so we just need the child ID to construct our index
AZ::EntityId childId;
EditorEntityInfoRequestBus::EventResult(childId, parentId, &EditorEntityInfoRequestBus::Events::GetChild, row);
return GetIndexFromEntity(childId, column);
AZ_Assert(childId.IsValid(), "No child found for parent");
return createIndex(row, column, static_cast<AZ::u64>(childId));
}
QVariant EntityOutlinerListModel::data(const QModelIndex& index, int role) const
@@ -517,13 +519,18 @@ namespace AzToolsFramework
{
AZ::EntityId parentId;
EditorEntityInfoRequestBus::EventResult(parentId, id, &EditorEntityInfoRequestBus::Events::GetParent);
return GetIndexFromEntity(parentId, index.column());
return GetIndexFromEntity(parentId, 0);
}
return QModelIndex();
}
Qt::ItemFlags EntityOutlinerListModel::flags(const QModelIndex& index) const
{
if (!index.isValid())
{
return Qt::ItemIsDropEnabled;
}
Qt::ItemFlags itemFlags = QAbstractItemModel::flags(index);
switch (index.column())
{
@@ -1208,6 +1215,10 @@ namespace AzToolsFramework
void EntityOutlinerListModel::ProcessEntityUpdates()
{
AZ_PROFILE_FUNCTION(Editor);
if (!m_entityChangeQueued)
{
return;
}
m_entityChangeQueued = false;
if (m_layoutResetQueued)
{
@@ -1236,31 +1247,14 @@ namespace AzToolsFramework
{
AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:ChangeQueue");
// its faster to just do a bulk data change than to carefully pick out indices
// so we'll just merge all ranges into a single range rather than try to make gaps
QModelIndex firstChangeIndex;
QModelIndex lastChangeIndex;
for (auto entityId : m_entityChangeQueue)
{
auto myIndex = GetIndexFromEntity(entityId, ColumnName);
if ((!firstChangeIndex.isValid())||(firstChangeIndex.row() > myIndex.row()))
if (entityId.IsValid())
{
firstChangeIndex = myIndex;
const QModelIndex beginIndex = GetIndexFromEntity(entityId, ColumnName);
const QModelIndex endIndex = createIndex(beginIndex.row(), VisibleColumnCount - 1, beginIndex.internalId());
emit dataChanged(beginIndex, endIndex);
}
if ((!lastChangeIndex.isValid())||(lastChangeIndex.row() < myIndex.row()))
{
// expand it to be the last column:
lastChangeIndex = myIndex;
}
}
if (firstChangeIndex.isValid())
{
// expand to cover all visible columns:
lastChangeIndex = createIndex(lastChangeIndex.row(), VisibleColumnCount - 1, lastChangeIndex.internalPointer());
emit dataChanged(firstChangeIndex, lastChangeIndex);
}
m_entityChangeQueue.clear();
@@ -1382,6 +1376,9 @@ namespace AzToolsFramework
m_isFilterDirty = true;
QueueAncestorUpdate(parentId);
emit EnableSelectionUpdates(true);
// Remove any pending updates for this removed entity.
m_entityChangeQueue.erase(childId);
}
void EntityOutlinerListModel::OnEntityInfoUpdatedOrderBegin(AZ::EntityId parentId, AZ::EntityId childId, AZ::u64 index)
@@ -145,6 +145,8 @@ namespace AzToolsFramework
void SetSortMode(EntityOutliner::DisplaySortMode sortMode) { m_sortMode = sortMode; }
void SetDropOperationInProgress(bool inProgress);
void ProcessEntityUpdates();
Q_SIGNALS:
void ExpandEntity(const AZ::EntityId& entityId, bool expand);
void SelectEntity(const AZ::EntityId& entityId, bool select);
@@ -178,7 +180,6 @@ namespace AzToolsFramework
void QueueEntityUpdate(AZ::EntityId entityId);
void QueueAncestorUpdate(AZ::EntityId entityId);
void QueueEntityToExpand(AZ::EntityId entityId, bool expand);
void ProcessEntityUpdates();
void ProcessEntityInfoResetEnd();
AZStd::unordered_set<AZ::EntityId> m_entitySelectQueue;
AZStd::unordered_set<AZ::EntityId> m_entityExpandQueue;
@@ -766,15 +766,8 @@ namespace AzToolsFramework
{
return false;
}
outPrefabAssetPath = product->GetRelativePath();
auto asset = AZ::Data::AssetManager::Instance().GetAsset(
product->GetAssetId(),
azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>(),
AZ::Data::AssetLoadBehavior::Default);
return asset.BlockUntilLoadComplete() != AZ::Data::AssetData::AssetStatus::Error;
return true;
}
void PrefabIntegrationManager::WarnUserOfError(AZStd::string_view title, AZStd::string_view message)
@@ -198,6 +198,8 @@ namespace AzToolsFramework
virtual float ManipulatorCircleBoundWidth() const = 0;
//! Returns if sticky select is enabled or not.
virtual bool StickySelectEnabled() const = 0;
//! Returns the default viewport camera position.
virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0;
protected:
~ViewportSettingsRequests() = default;
@@ -115,6 +115,33 @@ namespace AzToolsFramework
}
}
CursorEntityIdQuery::CursorEntityIdQuery(AZ::EntityId entityId, AZ::EntityId rootEntityId)
: m_entityId(entityId)
, m_containerAncestorEntityId(rootEntityId)
{
}
AZ::EntityId CursorEntityIdQuery::EntityIdUnderCursor() const
{
return m_entityId;
}
AZ::EntityId CursorEntityIdQuery::ContainerAncestorEntityId() const
{
return m_containerAncestorEntityId;
}
bool CursorEntityIdQuery::HasContainerAncestorEntityId() const
{
if (m_entityId.IsValid())
{
return m_entityId != m_containerAncestorEntityId;
}
return false;
}
EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
@@ -131,7 +158,7 @@ namespace AzToolsFramework
m_invalidClicks = AZStd::make_unique<InvalidClicks>(AZStd::move(invalidClicks));
}
AZ::EntityId EditorHelpers::HandleMouseInteraction(
CursorEntityIdQuery EditorHelpers::FindEntityIdUnderCursor(
const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@@ -202,17 +229,18 @@ namespace AzToolsFramework
m_invalidClicks->AddInvalidClick(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
}
return AZ::EntityId();
return CursorEntityIdQuery(AZ::EntityId(), AZ::EntityId());
}
// container entity support - if the entity that is being selected is part of a closed container,
// change the selection to the container instead.
if (ContainerEntityInterface* containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get())
{
return containerEntityInterface->FindHighestSelectableEntity(entityIdUnderCursor);
const auto highestSelectableEntity = containerEntityInterface->FindHighestSelectableEntity(entityIdUnderCursor);
return CursorEntityIdQuery(entityIdUnderCursor, highestSelectableEntity);
}
return entityIdUnderCursor;
return CursorEntityIdQuery(entityIdUnderCursor, AZ::EntityId());
}
void EditorHelpers::Display2d(
@@ -32,6 +32,28 @@ namespace AzToolsFramework
struct MouseInteractionEvent;
}
//!< Represents the result of a query to find the id of the entity under the cursor (if any).
class CursorEntityIdQuery
{
public:
CursorEntityIdQuery(AZ::EntityId entityId, AZ::EntityId rootEntityId);
//! Returns the entity id under the cursor (if any).
//! @note In the case of no entity id under the cursor, an invalid entity id is returned.
AZ::EntityId EntityIdUnderCursor() const;
//! Returns the topmost container entity id in the hierarchy if the entity id under the cursor is inside a container entity, otherwise returns the entity id.
//! @note In the case of no entity id under the cursor, an invalid entity id is returned.
AZ::EntityId ContainerAncestorEntityId() const;
//! Returns true if the query has a container ancestor entity id, otherwise false.
bool HasContainerAncestorEntityId() const;
private:
AZ::EntityId m_entityId; //<! The entity id under the cursor.
AZ::EntityId m_containerAncestorEntityId; //<! For entities in container entities, the topmost container entity id in the hierarchy, otherwise the entity id under the cursor.
};
//! EditorHelpers are the visualizations that appear for entities
//! when 'Display Helpers' is toggled on inside the editor.
//! These include but are not limited to entity icons and shape visualizations.
@@ -47,9 +69,9 @@ namespace AzToolsFramework
EditorHelpers& operator=(const EditorHelpers&) = delete;
~EditorHelpers() = default;
//! Handle any mouse interaction with the EditorHelpers.
//! Finds the id of the entity under the cursor (if any). For entities in container entities, also finds the topmost container entity id in the hierarchy.
//! Used to check if a particular entity was selected.
AZ::EntityId HandleMouseInteraction(
CursorEntityIdQuery FindEntityIdUnderCursor(
const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
//! Do the drawing responsible for the EditorHelpers.
@@ -80,7 +80,7 @@ namespace AzToolsFramework
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
m_cachedEntityIdUnderCursor = m_editorHelpers->FindEntityIdUnderCursor(cameraState, mouseInteraction).ContainerAncestorEntityId();
// when left clicking, if we successfully clicked an entity, assign that
// to the entity field selected in the entity inspector (RPE)
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Manipulators/ScaleManipulators.h>
#include <AzToolsFramework/Manipulators/TranslationManipulators.h>
#include <AzToolsFramework/Maths/TransformUtils.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorVisibilityBus.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
@@ -1799,7 +1800,8 @@ namespace AzToolsFramework
const AzFramework::ViewportId viewportId = mouseInteraction.m_mouseInteraction.m_interactionId.m_viewportId;
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
const auto cursorEntityIdQuery = m_editorHelpers->FindEntityIdUnderCursor(cameraState, mouseInteraction);
m_cachedEntityIdUnderCursor = cursorEntityIdQuery.ContainerAncestorEntityId();
const auto selectClickEvent = ClickDetectorEventFromViewportInteraction(mouseInteraction);
m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
@@ -1825,8 +1827,6 @@ namespace AzToolsFramework
}
}
const AZ::EntityId entityIdUnderCursor = m_cachedEntityIdUnderCursor;
EditorContextMenuUpdate(m_contextMenu, mouseInteraction);
m_boxSelect.HandleMouseInteraction(mouseInteraction);
@@ -1842,6 +1842,21 @@ namespace AzToolsFramework
return true;
}
const AZ::EntityId entityIdUnderCursor = m_cachedEntityIdUnderCursor;
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick &&
mouseInteraction.m_mouseInteraction.m_mouseButtons.Left())
{
if (cursorEntityIdQuery.HasContainerAncestorEntityId())
{
if (auto prefabFocusInterface = AZ::Interface<Prefab::PrefabFocusInterface>::Get())
{
prefabFocusInterface->FocusOnPrefabInstanceOwningEntityId(cursorEntityIdQuery.ContainerAncestorEntityId());
return false;
}
}
}
bool stickySelect = false;
ViewportInteraction::ViewportSettingsRequestBus::EventResult(
stickySelect, viewportId, &ViewportInteraction::ViewportSettingsRequestBus::Events::StickySelectEnabled);
@@ -0,0 +1,214 @@
/*
* 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/Serialization/SerializeContext.h>
#include <AzTest/AzTest.h>
#include <AzFramework/Entity/EntityContextBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx>
#include <AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h>
#include <AzToolsFramework/Undo/UndoSystem.h>
#include <Prefab/PrefabTestFixture.h>
#include <QAbstractItemModelTester>
namespace UnitTest
{
// Test fixture for the entity outliner model that uses a QAbstractItemModelTester to validate the state of the model
// when QAbstractItemModel signals fire. Tests will exit with a fatal error if an invalid state is detected.
class EntityOutlinerTest : public PrefabTestFixture
{
protected:
void SetUpEditorFixtureImpl() override
{
PrefabTestFixture::SetUpEditorFixtureImpl();
GetApplication()->RegisterComponentDescriptor(AzToolsFramework::EditorEntityContextComponent::CreateDescriptor());
m_model = AZStd::make_unique<AzToolsFramework::EntityOutlinerListModel>();
m_model->Initialize();
m_modelTester =
AZStd::make_unique<QAbstractItemModelTester>(m_model.get(), QAbstractItemModelTester::FailureReportingMode::Fatal);
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
m_undoStack, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetUndoStack);
AZ_Assert(m_undoStack, "Failed to look up undo stack from tools application");
// Create a new root prefab - the synthetic "NewLevel.prefab" that comes in by default isn't suitable for outliner tests
// because it's created before the EditorEntityModel that our EntityOutlinerListModel subscribes to, and we want to
// recreate it as part of the fixture regardless.
auto entityOwnershipService = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
entityOwnershipService->CreateNewLevelPrefab("UnitTestRoot.prefab", "");
}
void TearDownEditorFixtureImpl() override
{
m_undoStack = nullptr;
m_modelTester.reset();
m_model.reset();
PrefabTestFixture::TearDownEditorFixtureImpl();
}
// Creates an entity with a given name as one undoable operation
// Parents to parentId, or the root prefab container entity if parentId is invalid
AZ::EntityId CreateNamedEntity(AZStd::string name, AZ::EntityId parentId = AZ::EntityId())
{
auto createResult = m_prefabPublicInterface->CreateEntity(parentId, AZ::Vector3());
AZ_Assert(createResult.IsSuccess(), "Failed to create entity: %s", createResult.GetError().c_str());
AZ::EntityId entityId = createResult.GetValue();
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
entity->Deactivate();
entity->SetName(name);
// Normally, in invalid parent ID should automatically parent us to the root prefab, but currently in the unit test
// environment entities aren't created with a default transform component, so CreateEntity won't correctly parent.
// We get the actual target parent ID here, then create our missing transform component.
if (!parentId.IsValid())
{
auto prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
parentId = prefabEditorEntityOwnershipInterface->GetRootPrefabInstance()->get().GetContainerEntityId();
}
auto transform = aznew AzToolsFramework::Components::TransformComponent;
transform->SetParent(parentId);
entity->AddComponent(transform);
entity->Activate();
// Update our undo cache entry to include the rename / reparent as one atomic operation.
m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop());
ProcessDeferredUpdates();
return entityId;
}
// Helper to visualize debug state
void PrintModel()
{
AZStd::deque<AZStd::pair<QModelIndex, int>> indices;
indices.push_back({ m_model->index(0, 0), 0 });
while (!indices.empty())
{
auto [index, depth] = indices.front();
indices.pop_front();
QString indentString;
for (int i = 0; i < depth; ++i)
{
indentString += " ";
}
qDebug() << (indentString + index.data(Qt::DisplayRole).toString()) << index.internalId();
for (int i = 0; i < m_model->rowCount(index); ++i)
{
indices.emplace_back(m_model->index(i, 0, index), depth + 1);
}
}
};
// Gets the index of the root prefab, i.e. the "New Level" container entity
QModelIndex GetRootIndex() const
{
return m_model->index(0, 0);
}
// Kicks off any updates scheduled for the next tick
void ProcessDeferredUpdates()
{
// Force a prefab propagation for updates that are deferred to the next tick.
m_prefabSystemComponent->OnSystemTick();
// Ensure the model process its entity update queue
m_model->ProcessEntityUpdates();
}
// Performs an undo operation and ensures the tick-scheduled updates happen
void Undo()
{
m_undoStack->Undo();
ProcessDeferredUpdates();
}
// Performs a redo operation and ensures the tick-scheduled updates happen
void Redo()
{
m_undoStack->Redo();
ProcessDeferredUpdates();
}
AZStd::unique_ptr<AzToolsFramework::EntityOutlinerListModel> m_model;
AZStd::unique_ptr<QAbstractItemModelTester> m_modelTester;
AzToolsFramework::UndoSystem::UndoStack* m_undoStack = nullptr;
};
TEST_F(EntityOutlinerTest, TestCreateFlatHierarchyUndoAndRedoWorks)
{
constexpr size_t entityCount = 10;
for (size_t i = 0; i < entityCount; ++i)
{
CreateNamedEntity(AZStd::string::format("Entity%zu", i));
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
}
for (int i = entityCount; i > 0; --i)
{
Undo();
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i - 1);
}
for (size_t i = 0; i < entityCount; ++i)
{
Redo();
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
}
}
TEST_F(EntityOutlinerTest, TestCreateNestedHierarchyUndoAndRedoWorks)
{
constexpr size_t depth = 5;
auto modelDepth = [this]() -> int
{
int depth = 0;
QModelIndex index = GetRootIndex();
while (m_model->rowCount(index) > 0)
{
++depth;
index = m_model->index(0, 0, index);
}
return depth;
};
AZ::EntityId parentId;
for (int i = 0; i < depth; i++)
{
parentId = CreateNamedEntity(AZStd::string::format("EntityDepth%i", i), parentId);
EXPECT_EQ(modelDepth(), i + 1);
}
for (int i = depth - 1; i >= 0; --i)
{
Undo();
EXPECT_EQ(modelDepth(), i);
}
for (int i = 0; i < depth; ++i)
{
Redo();
EXPECT_EQ(modelDepth(), i + 1);
}
}
} // namespace UnitTest
@@ -120,6 +120,8 @@ set(FILES
ToolsComponents/EditorLayerComponentTests.cpp
ToolsComponents/EditorTransformComponentTests.cpp
TransformComponent.cpp
UI/EntityIdQLineEditTests.cpp
UI/EntityOutlinerTests.cpp
UI/EntityPropertyEditorTests.cpp
UndoStack.cpp
Viewport/ClusterTests.cpp
@@ -3573,19 +3573,61 @@ namespace AssetProcessor
QString knownPathBeforeWildcard = encodedFileData.left(slashBeforeWildcardIndex + 1); // include the slash
QString relativeSearch = encodedFileData.mid(slashBeforeWildcardIndex + 1); // skip the slash
for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
// Absolute path, just check the 1 scan folder
if (AZ::IO::PathView(encodedFileData.toUtf8().constData()).IsAbsolute())
{
const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i);
if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/"))
QString scanFolderName;
if (!m_platformConfig->ConvertToRelativePath(encodedFileData, resultDatabaseSourceName, scanFolderName))
{
continue;
AZ_Warning(
AssetProcessor::ConsoleChannel, false,
"'%s' does not appear to be in any input folder. Use relative paths instead.",
sourceDependency.m_sourceFileDependencyPath.c_str());
}
QDir rooted(scanFolderInfo->ScanPath());
QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard);
auto scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolderName);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
// Make an absolute path that is ScanFolderPath + Part of search path before the wildcard
QDir rooted(scanFolderName);
QString scanFolderAndKnownSubPath = rooted.absoluteFilePath(knownPathBeforeWildcard);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
scanFolderAndKnownSubPath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
}
else // Relative path, check every scan folder
{
for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
{
const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i);
if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/"))
{
continue;
}
QDir rooted(scanFolderInfo->ScanPath());
QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
}
}
// Convert to relative paths
for (auto dependencyItr = resolvedDependencyList.begin(); dependencyItr != resolvedDependencyList.end();)
{
QString relativePath, scanFolder;
if (m_platformConfig->ConvertToRelativePath(*dependencyItr, relativePath, scanFolder))
{
*dependencyItr = relativePath;
++dependencyItr;
}
else
{
AZ_Warning("AssetProcessor", false, "Failed to get relative path for wildcard dependency file %s. Is the file within a scan folder?",
dependencyItr->toUtf8().constData());
dependencyItr = resolvedDependencyList.erase(dependencyItr);
}
}
resultDatabaseSourceName = encodedFileData.replace('\\', '/');
@@ -1435,32 +1435,35 @@ namespace AssetProcessor
return QString();
}
QStringList PlatformConfiguration::FindWildcardMatches(const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const
QStringList PlatformConfiguration::FindWildcardMatches(
const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const
{
if (relativeName.isEmpty())
{
return QStringList();
}
const int pathLen = sourceFolder.length() + 1;
QDir sourceFolderDir(sourceFolder);
relativeName.replace('\\', '/');
QString posixRelativeName = QDir::fromNativeSeparators(relativeName);
QStringList returnList;
QRegExp nameMatch{ relativeName, Qt::CaseInsensitive, QRegExp::Wildcard };
QDirIterator diretoryIterator(sourceFolder, QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
QRegExp nameMatch{ posixRelativeName, Qt::CaseInsensitive, QRegExp::Wildcard };
QDirIterator dirIterator(
sourceFolderDir.path(), QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot,
recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
QStringList files;
while (diretoryIterator.hasNext())
while (dirIterator.hasNext())
{
diretoryIterator.next();
if (!includeFolders && !diretoryIterator.fileInfo().isFile())
dirIterator.next();
if (!includeFolders && !dirIterator.fileInfo().isFile())
{
continue;
}
QString pathMatch{ diretoryIterator.filePath().mid(pathLen) };
QString pathMatch{ sourceFolderDir.relativeFilePath(dirIterator.filePath()) };
if (nameMatch.exactMatch(pathMatch))
{
returnList.append(AssetUtilities::NormalizeFilePath(diretoryIterator.filePath()));
returnList.append(QDir::fromNativeSeparators(dirIterator.filePath()));
}
}
return returnList;
@@ -7,7 +7,7 @@
*/
#include <GemRepo/GemRepoAddDialog.h>
#include <FormLineEditWidget.h>
#include <FormFolderBrowseEditWidget.h>
#include <QVBoxLayout>
#include <QLabel>
@@ -40,7 +40,7 @@ namespace O3DE::ProjectManager
instructionContextLabel->setAlignment(Qt::AlignLeft);
vLayout->addWidget(instructionContextLabel);
m_repoPath = new FormLineEditWidget(tr("Repository Path"), "", this);
m_repoPath = new FormFolderBrowseEditWidget(tr("Repository Path"), "", this);
m_repoPath->setFixedWidth(600);
vLayout->addWidget(m_repoPath);
@@ -36,7 +36,7 @@ namespace O3DE::ProjectManager
QString m_summary = "No summary provided.";
QString m_additionalInfo = "";
QString m_directoryLink = "";
QString m_repoLink = "";
QString m_repoUri = "";
QStringList m_includedGemPaths = {};
QDateTime m_lastUpdated;
};
@@ -60,8 +60,8 @@ namespace O3DE::ProjectManager
// Repo name and url link
m_nameLabel->setText(m_model->GetName(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoLink(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoLink(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoUri(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoUri(modelIndex));
// Repo summary
m_summaryLabel->setText(m_model->GetSummary(modelIndex));
@@ -145,6 +145,11 @@ namespace O3DE::ProjectManager
GemRepoModel::SetEnabled(*model, modelIndex, !isAdded);
return true;
}
else if (keyEvent->key() == Qt::Key_X)
{
emit RemoveRepo(modelIndex);
return true;
}
}
if (event->type() == QEvent::MouseButtonPress)
@@ -154,6 +159,7 @@ namespace O3DE::ProjectManager
QRect fullRect, itemRect, contentRect;
CalcRects(option, fullRect, itemRect, contentRect);
const QRect buttonRect = CalcButtonRect(contentRect);
const QRect deleteButtonRect = CalcDeleteButtonRect(contentRect);
if (buttonRect.contains(mouseEvent->pos()))
{
@@ -161,6 +167,11 @@ namespace O3DE::ProjectManager
GemRepoModel::SetEnabled(*model, modelIndex, !isAdded);
return true;
}
else if (deleteButtonRect.contains(mouseEvent->pos()))
{
emit RemoveRepo(modelIndex);
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
@@ -214,9 +225,14 @@ namespace O3DE::ProjectManager
painter->restore();
}
QRect GemRepoItemDelegate::CalcDeleteButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2);
return QRect(topLeft, QSize(s_iconSize, s_iconSize));
}
void GemRepoItemDelegate::DrawEditButtons(QPainter* painter, const QRect& contentRect) const
{
painter->drawPixmap(contentRect.right() - s_iconSize * 2 - s_iconSpacing, contentRect.center().y() - s_iconSize / 2, m_editIcon);
painter->drawPixmap(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2, m_deleteIcon);
}
@@ -66,10 +66,14 @@ namespace O3DE::ProjectManager
inline constexpr static int s_refreshIconSize = 14;
inline constexpr static int s_refreshIconSpacing = 10;
signals:
void RemoveRepo(const QModelIndex& modelIndex);
protected:
void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const;
QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const;
QRect CalcButtonRect(const QRect& contentRect) const;
QRect CalcDeleteButtonRect(const QRect& contentRect) const;
void DrawButton(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const;
void DrawEditButtons(QPainter* painter, const QRect& contentRect) const;
@@ -9,6 +9,8 @@
#include <GemRepo/GemRepoListView.h>
#include <GemRepo/GemRepoItemDelegate.h>
#include <QShortcut>
namespace O3DE::ProjectManager
{
GemRepoListView::GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
@@ -19,6 +21,9 @@ namespace O3DE::ProjectManager
setModel(model);
setSelectionModel(selectionModel);
setItemDelegate(new GemRepoItemDelegate(model, this));
GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, this);
connect(itemDelegate, &GemRepoItemDelegate::RemoveRepo, this, &GemRepoListView::RemoveRepo);
setItemDelegate(itemDelegate);
}
} // namespace O3DE::ProjectManager
@@ -25,5 +25,8 @@ namespace O3DE::ProjectManager
public:
explicit GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
~GemRepoListView() = default;
signals:
void RemoveRepo(const QModelIndex& modelIndex);
};
} // namespace O3DE::ProjectManager
@@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
item->setData(gemRepoInfo.m_summary, RoleSummary);
item->setData(gemRepoInfo.m_isEnabled, RoleIsEnabled);
item->setData(gemRepoInfo.m_directoryLink, RoleDirectoryLink);
item->setData(gemRepoInfo.m_repoLink, RoleRepoLink);
item->setData(gemRepoInfo.m_repoUri, RoleRepoUri);
item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated);
item->setData(gemRepoInfo.m_path, RolePath);
item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo);
@@ -83,9 +83,9 @@ namespace O3DE::ProjectManager
return modelIndex.data(RoleDirectoryLink).toString();
}
QString GemRepoModel::GetRepoLink(const QModelIndex& modelIndex)
QString GemRepoModel::GetRepoUri(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleRepoLink).toString();
return modelIndex.data(RoleRepoUri).toString();
}
QDateTime GemRepoModel::GetLastUpdated(const QModelIndex& modelIndex)
@@ -35,7 +35,7 @@ namespace O3DE::ProjectManager
static QString GetSummary(const QModelIndex& modelIndex);
static QString GetAdditionalInfo(const QModelIndex& modelIndex);
static QString GetDirectoryLink(const QModelIndex& modelIndex);
static QString GetRepoLink(const QModelIndex& modelIndex);
static QString GetRepoUri(const QModelIndex& modelIndex);
static QDateTime GetLastUpdated(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex);
@@ -55,7 +55,7 @@ namespace O3DE::ProjectManager
RoleSummary,
RoleIsEnabled,
RoleDirectoryLink,
RoleRepoLink,
RoleRepoUri,
RoleLastUpdated,
RolePath,
RoleAdditionalInfo,
@@ -25,6 +25,7 @@
#include <QTableWidget>
#include <QFrame>
#include <QStackedWidget>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
@@ -79,21 +80,48 @@ namespace O3DE::ProjectManager
if (repoAddDialog->exec() == QDialog::DialogCode::Accepted)
{
QString repoUrl = repoAddDialog->GetRepoPath();
if (repoUrl.isEmpty())
QString repoUri = repoAddDialog->GetRepoPath();
if (repoUri.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
return;
}
AZ::Outcome<void, AZStd::string> addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUrl);
if (addGemRepoResult.IsSuccess())
bool addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
if (addGemRepoResult)
{
Reinit();
}
else
{
QMessageBox::critical(this, tr("Operation failed"),
QString("Failed to add gem repo: %1.<br>Error:<br>%2").arg(repoUrl, addGemRepoResult.GetError().c_str()));
QString failureMessage = tr("Failed to add gem repo: %1.").arg(repoUri);
QMessageBox::critical(this, tr("Operation failed"), failureMessage);
AZ_Error("Project Manger", false, failureMessage.toUtf8());
}
}
}
void GemRepoScreen::HandleRemoveRepoButton(const QModelIndex& modelIndex)
{
QString repoName = m_gemRepoModel->GetName(modelIndex);
QMessageBox::StandardButton warningResult = QMessageBox::warning(
this, tr("Remove Repo"), tr("Are you sure you would like to remove gem repo: %1?").arg(repoName),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::Yes)
{
QString repoUri = m_gemRepoModel->GetRepoUri(modelIndex);
bool removeGemRepoResult = PythonBindingsInterface::Get()->RemoveGemRepo(repoUri);
if (removeGemRepoResult)
{
Reinit();
}
else
{
QString failureMessage = tr("Failed to remove gem repo: %1.").arg(repoUri);
QMessageBox::critical(this, tr("Operation failed"), failureMessage);
AZ_Error("Project Manger", false, failureMessage.toUtf8());
}
}
}
@@ -251,6 +279,8 @@ namespace O3DE::ProjectManager
m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), this);
middleVLayout->addWidget(m_gemRepoListView);
connect(m_gemRepoListView, &GemRepoListView::RemoveRepo, this, &GemRepoScreen::HandleRemoveRepoButton);
hLayout->addLayout(middleVLayout);
m_gemRepoInspector = new GemRepoInspector(m_gemRepoModel, this);
@@ -39,6 +39,7 @@ namespace O3DE::ProjectManager
public slots:
void HandleAddRepoButton();
void HandleRemoveRepoButton(const QModelIndex& modelIndex);
private:
void FillModel();
@@ -939,17 +939,60 @@ namespace O3DE::ProjectManager
}
}
AZ::Outcome<void, AZStd::string> PythonBindings::AddGemRepo(const QString& repoUri)
bool PythonBindings::AddGemRepo(const QString& repoUri)
{
// o3de scripts need method added
(void)repoUri;
return AZ::Failure<AZStd::string>("Adding Gem Repo not implemented yet in o3de scripts.");
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
auto pyUri = QString_To_Py_String(repoUri);
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pyUri);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
bool PythonBindings::RemoveGemRepo(const QString& repoUri)
{
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path
pybind11::none(), // project_path
pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
QString_To_Py_String(repoUri), // repo_uri
pybind11::none(), // default_engines_folder
pybind11::none(), // default_projects_folder
pybind11::none(), // default_gems_folder
pybind11::none(), // default_templates_folder
pybind11::none(), // default_restricted_folder
pybind11::none(), // default_third_party_folder
pybind11::none(), // external_subdir_engine_path
pybind11::none(), // external_subdir_project_path
true, // remove
false // force
);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
GemRepoInfo PythonBindings::GetGemRepoInfo(pybind11::handle repoUri)
{
GemRepoInfo gemRepoInfo;
gemRepoInfo.m_repoLink = Py_To_String(repoUri);
gemRepoInfo.m_repoUri = Py_To_String(repoUri);
auto data = m_manifest.attr("get_repo_json_data")(repoUri);
if (pybind11::isinstance<pybind11::dict>(data))
@@ -957,7 +1000,7 @@ namespace O3DE::ProjectManager
try
{
// required
gemRepoInfo.m_repoLink = Py_To_String(data["repo_uri"]);
gemRepoInfo.m_repoUri = Py_To_String(data["repo_uri"]);
gemRepoInfo.m_name = Py_To_String(data["repo_name"]);
gemRepoInfo.m_creator = Py_To_String(data["origin"]);
@@ -1019,13 +1062,13 @@ namespace O3DE::ProjectManager
#else
GemRepoInfo mockJohnRepo("JohnCreates", "John Smith", QDateTime(QDate(2021, 8, 31), QTime(11, 57)), true);
mockJohnRepo.m_summary = "John's Summary. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitudin dapibus urna";
mockJohnRepo.m_repoLink = "https://github.com/o3de/o3de";
mockJohnRepo.m_repoUri = "https://github.com/o3de/o3de";
mockJohnRepo.m_additionalInfo = "John's additional info. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitu.";
gemRepos.push_back(mockJohnRepo);
GemRepoInfo mockJaneRepo("JanesGems", "Jane Doe", QDateTime(QDate(2021, 9, 10), QTime(18, 23)), false);
mockJaneRepo.m_summary = "Jane's Summary.";
mockJaneRepo.m_repoLink = "https://github.com/o3de/o3de.org";
mockJaneRepo.m_repoUri = "https://github.com/o3de/o3de.org";
gemRepos.push_back(mockJaneRepo);
#endif // MOCK_GEM_REPO_INFO
@@ -58,7 +58,8 @@ namespace O3DE::ProjectManager
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) override;
// Gem Repos
AZ::Outcome<void, AZStd::string> AddGemRepo(const QString& repoUri) override;
bool AddGemRepo(const QString& repoUri) override;
bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
private:
@@ -169,11 +169,18 @@ namespace O3DE::ProjectManager
// Gem Repos
/**
* A gem repo to engine. Registers this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo manifest file.
* @return An outcome with the success flag as well as an error message in case of a failure.
* Registers this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo.
* @return true on success, false on failure.
*/
virtual AZ::Outcome<void, AZStd::string> AddGemRepo(const QString& repoUri) = 0;
virtual bool AddGemRepo(const QString& repoUri) = 0;
/**
* Unregisters this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo.
* @return true on success, false on failure.
*/
virtual bool RemoveGemRepo(const QString& repoUri) = 0;
/**
* Get all available gem repo infos. Gathers all repos registered with the engine.
@@ -51,4 +51,21 @@ namespace AWSCore
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-metrics/advanced-topics/";
static constexpr const char AWSMetricsSettingsUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-metrics/";
static constexpr const char AWSGameLiftGemOverviewUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/";
static constexpr const char AWSGameLiftGemSetupUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/gem-setup/";
static constexpr const char AWSGameLiftScriptingUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/scripting/";
static constexpr const char AWSGameLiftAPIReferenceUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/cpp-api/";
static constexpr const char AWSGameLiftAdvancedTopicsUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/advanced-topics/";
static constexpr const char AWSGameLiftLocalTestingUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/local-testing/";
static constexpr const char AWSGameLiftBuildPackagingUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/build-packaging-for-windows/";
static constexpr const char AWSGameLiftResourceManagementUrl[] =
"https://o3de.org/docs/user-guide/gems/reference/aws/aws-gamelift/resource-management/";
} // namespace AWSCore
@@ -38,4 +38,14 @@ namespace AWSCore
static constexpr const char AWSMetricsAPIReferenceActionText[] = "API reference";
static constexpr const char AWSMetricsAdvancedTopicsActionText[] = "Advanced topics";
static constexpr const char AWSMetricsSettingsActionText[] = "Metrics settings";
static constexpr const char AWSGameLiftActionText[] = "GameLift Gem";
static constexpr const char AWSGameLiftGemOverviewActionText[] = "Gem overview";
static constexpr const char AWSGameLiftGemSetupActionText[] = "Setup";
static constexpr const char AWSMGameLiftScriptingActionText[] = "Scripting reference";
static constexpr const char AWSGameLiftAPIReferenceActionText[] = "API reference";
static constexpr const char AWSGameLiftAdvancedTopicsActionText[] = "Advanced topics";
static constexpr const char AWSGameLiftLocalTestingActionText[] = "Local testing";
static constexpr const char AWSGameLiftBuildPackagingActionText[] = "Build packaging (Windows)";
static constexpr const char AWSGameLiftResourceManagementActionText[] = "Resource Management";
} // namespace AWSCore
@@ -48,6 +48,7 @@ namespace AWSCore
// AWSCoreEditorRequestBus interface implementation
void SetAWSClientAuthEnabled() override;
void SetAWSMetricsEnabled() override;
void SetAWSGameLiftEnabled() override;
QMenu* SetAWSFeatureSubMenu(const AZStd::string& menuText);
@@ -53,6 +53,7 @@ namespace AWSCore
public:
virtual void SetAWSClientAuthEnabled() = 0;
virtual void SetAWSMetricsEnabled() = 0;
virtual void SetAWSGameLiftEnabled() = 0;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
@@ -156,6 +156,11 @@ namespace AWSCore
metrics->setIcon(QIcon(QString(":/Notifications/download.svg")));
metrics->setDisabled(true);
this->addAction(metrics);
QAction* gamelift = new QAction(QObject::tr(AWSGameLiftActionText));
gamelift->setIcon(QIcon(QString(":/Notifications/download.svg")));
gamelift->setDisabled(true);
this->addAction(gamelift);
}
void AWSCoreEditorMenu::SetAWSClientAuthEnabled()
@@ -181,6 +186,23 @@ namespace AWSCore
AddSpaceForIcon(subMenu);
}
void AWSCoreEditorMenu::SetAWSGameLiftEnabled()
{
// TODO: instead of creating submenu in core editor, aws feature gem should return submenu component directly
QMenu* subMenu = SetAWSFeatureSubMenu(AWSGameLiftActionText);
subMenu->addAction(AddExternalLinkAction(AWSGameLiftGemOverviewActionText, AWSGameLiftGemOverviewUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftGemSetupActionText, AWSGameLiftGemSetupUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSMGameLiftScriptingActionText, AWSGameLiftScriptingUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftAPIReferenceActionText, AWSGameLiftAPIReferenceUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftAdvancedTopicsActionText, AWSGameLiftAdvancedTopicsUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftLocalTestingActionText, AWSGameLiftLocalTestingUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftBuildPackagingActionText, AWSGameLiftBuildPackagingUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(AWSGameLiftResourceManagementActionText, AWSGameLiftResourceManagementUrl, ":/Notifications/link.svg"));
AddSpaceForIcon(subMenu);
}
void AWSCoreEditorMenu::SetAWSMetricsEnabled()
{
// TODO: instead of creating submenu in core editor, aws feature gem should return submenu component directly
@@ -21,8 +21,8 @@
using namespace AWSCore;
static constexpr const int ExpectedActionNumOnWindowsPlatform = 8;
static constexpr const int ExpectedActionNumOnOtherPlatform = 6;
static constexpr const int ExpectedActionNumOnWindowsPlatform = 9;
static constexpr const int ExpectedActionNumOnOtherPlatform = 7;
class AWSCoreEditorMenuTest
: public AWSCoreFixture
@@ -60,6 +60,7 @@ TEST_F(AWSCoreEditorMenuTest, AWSCoreEditorMenu_BroadcastFeatureGemsAreEnabled_C
AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSClientAuthEnabled);
AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSMetricsEnabled);
AWSCoreEditorRequestBus::Broadcast(&AWSCoreEditorRequests::SetAWSGameLiftEnabled);
QList<QAction*> actualActions = testMenu.actions();
for (QList<QAction*>::iterator itr = actualActions.begin(); itr != actualActions.end(); itr++)
@@ -73,5 +74,10 @@ TEST_F(AWSCoreEditorMenuTest, AWSCoreEditorMenu_BroadcastFeatureGemsAreEnabled_C
{
EXPECT_TRUE((*itr)->isEnabled());
}
if (QString::compare((*itr)->text(), AWSGameLiftActionText) == 0)
{
EXPECT_TRUE((*itr)->isEnabled());
}
}
}
@@ -94,4 +94,41 @@ namespace AWSGameLift
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftMatchmakingRequestBus = AZ::EBus<AzFramework::IMatchmakingRequests, AWSGameLiftMatchmakingRequests>;
//! IAWSGameLiftMatchmakingEventRequests
//! GameLift Gem matchmaking event interfaces which is used to track matchmaking ticket event
//! Developer should define the way to poll matchmaking ticket event and behavior based on the ticket status
//! Use AWSGameLiftClientLocalTicketTracker as an example, it uses continuous polling to query matchmaking ticket:
//! StartPolling - local ticket tracker starts monitor process for matchmaking ticket, and joins player
//! to the match once ticket is complete
//! StopPolling - local ticket tracker cancels ongoing matchmaking ticket and stops monitoring process
class IAWSGameLiftMatchmakingEventRequests
{
public:
AZ_RTTI(IAWSGameLiftMatchmakingEventRequests, "{C2DA440E-74E0-411E-813D-5880B50B0C9E}");
IAWSGameLiftMatchmakingEventRequests() = default;
virtual ~IAWSGameLiftMatchmakingEventRequests() = default;
//! StartPolling
//! Request to start process for polling matchmaking ticket based on given ticket id and player Id
//! @param ticketId The requested matchmaking ticket id
//! @param playerId The requested matchmaking player id
virtual void StartPolling(const AZStd::string& ticketId, const AZStd::string& playerId) = 0;
//! StopPolling
//! Request to stop process for polling matchmaking ticket
virtual void StopPolling() = 0;
};
// IAWSGameLiftMatchmakingEventRequests EBus wrapper for scripting
class AWSGameLiftMatchmakingEventRequests
: public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
};
using AWSGameLiftMatchmakingEventRequestBus = AZ::EBus<IAWSGameLiftMatchmakingEventRequests, AWSGameLiftMatchmakingEventRequests>;
} // namespace AWSGameLift
@@ -30,12 +30,14 @@ namespace AWSGameLift
void AWSGameLiftClientLocalTicketTracker::ActivateTracker()
{
AZ::Interface<IAWSGameLiftMatchmakingInternalRequests>::Register(this);
AZ::Interface<IAWSGameLiftMatchmakingEventRequests>::Register(this);
AWSGameLiftMatchmakingEventRequestBus::Handler::BusConnect();
}
void AWSGameLiftClientLocalTicketTracker::DeactivateTracker()
{
AZ::Interface<IAWSGameLiftMatchmakingInternalRequests>::Unregister(this);
AWSGameLiftMatchmakingEventRequestBus::Handler::BusDisconnect();
AZ::Interface<IAWSGameLiftMatchmakingEventRequests>::Unregister(this);
StopPolling();
}
@@ -12,7 +12,7 @@
#include <AzCore/std/parallel/mutex.h>
#include <AzCore/std/parallel/thread.h>
#include <Request/IAWSGameLiftMatchmakingInternalRequests.h>
#include <Request/IAWSGameLiftRequests.h>
#include <aws/gamelift/model/MatchmakingTicket.h>
@@ -30,7 +30,7 @@ namespace AWSGameLift
//! For use in production, please see GameLifts guidance about matchmaking at volume.
//! The continuous polling approach here is only suitable for low volume matchmaking and is meant to aid with development only
class AWSGameLiftClientLocalTicketTracker
: public IAWSGameLiftMatchmakingInternalRequests
: public AWSGameLiftMatchmakingEventRequestBus::Handler
{
public:
static constexpr const char AWSGameLiftClientLocalTicketTrackerName[] = "AWSGameLiftClientLocalTicketTracker";
@@ -44,7 +44,7 @@ namespace AWSGameLift
virtual void ActivateTracker();
virtual void DeactivateTracker();
// IAWSGameLiftMatchmakingInternalRequests interface implementation
// AWSGameLiftMatchmakingEventRequestBus interface implementation
void StartPolling(const AZStd::string& ticketId, const AZStd::string& playerId) override;
void StopPolling() override;
@@ -12,6 +12,8 @@
#include <AzCore/Serialization/EditContextConstants.inl>
#include <AzFramework/Session/SessionConfig.h>
#include <AWSGameLiftClientLocalTicketTracker.h>
#include <AWSCoreBus.h>
#include <AWSGameLiftClientManager.h>
#include <AWSGameLiftClientSystemComponent.h>
#include <Request/AWSGameLiftAcceptMatchRequest.h>
@@ -59,10 +61,17 @@ namespace AWSGameLift
behaviorContext->EBus<AWSGameLiftRequestBus>("AWSGameLiftRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Event("ConfigureGameLiftClient", &AWSGameLiftRequestBus::Events::ConfigureGameLiftClient,
{{{"Region", ""}}})
{ { { "Region", "" } } })
->Event("CreatePlayerId", &AWSGameLiftRequestBus::Events::CreatePlayerId,
{{{"IncludeBrackets", ""},
{"IncludeDashes", ""}}});
{ { { "IncludeBrackets", "" },
{ "IncludeDashes", "" } } });
behaviorContext->EBus<AWSGameLiftMatchmakingEventRequestBus>("AWSGameLiftMatchmakingEventRequestBus")
->Attribute(AZ::Script::Attributes::Category, "AWSGameLift")
->Event("StartPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StartPolling,
{ { { "TicketId", "" },
{ "PlayerId", "" } } })
->Event("StopPolling", &AWSGameLiftMatchmakingEventRequestBus::Events::StopPolling);
}
}
@@ -97,6 +106,8 @@ namespace AWSGameLift
m_gameliftClient.reset();
m_gameliftManager->ActivateManager();
m_gameliftTicketTracker->ActivateTracker();
AWSCore::AWSCoreEditorRequestBus::Broadcast(&AWSCore::AWSCoreEditorRequests::SetAWSGameLiftEnabled);
}
void AWSGameLiftClientSystemComponent::Deactivate()
@@ -11,12 +11,12 @@
#include <AzCore/Component/Component.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AWSGameLiftClientLocalTicketTracker.h>
#include <Request/IAWSGameLiftInternalRequests.h>
namespace AWSGameLift
{
class AWSGameLiftClientManager;
class AWSGameLiftClientLocalTicketTracker;
//! Gem client system component. Responsible for creating the gamelift client manager.
class AWSGameLiftClientSystemComponent
@@ -33,6 +33,11 @@ namespace AWSGameLift
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<AzFramework::AcceptMatchRequest>("AcceptMatchRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
// Expose base type to BehaviorContext, but hide it to be used directly
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All);
behaviorContext->Class<AWSGameLiftAcceptMatchRequest>("AWSGameLiftAcceptMatchRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Property("AcceptMatch", BehaviorValueProperty(&AWSGameLiftAcceptMatchRequest::m_acceptMatch))
@@ -40,6 +40,11 @@ namespace AWSGameLift
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<AzFramework::StartMatchmakingRequest>("StartMatchmakingRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
// Expose base type to BehaviorContext, but hide it to be used directly
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All);
behaviorContext->Class<AWSGameLiftStartMatchmakingRequest>("AWSGameLiftStartMatchmakingRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Property("TicketId", BehaviorValueProperty(&AWSGameLiftStartMatchmakingRequest::m_ticketId))
@@ -33,6 +33,11 @@ namespace AWSGameLift
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<AzFramework::StopMatchmakingRequest>("StopMatchmakingRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
// Expose base type to BehaviorContext, but hide it to be used directly
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All);
behaviorContext->Class<AWSGameLiftStopMatchmakingRequest>("AWSGameLiftStopMatchmakingRequest")
->Attribute(AZ::Script::Attributes::Storage, AZ::Script::Attributes::StorageType::Value)
->Property("TicketId", BehaviorValueProperty(&AWSGameLiftStopMatchmakingRequest::m_ticketId));
@@ -1,39 +0,0 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>
namespace AWSGameLift
{
//! IAWSGameLiftMatchmakingInternalRequests
//! GameLift Gem matchmaking internal interfaces which is used to communicate
//! with client side ticket tracker to sync matchmaking ticket data and join
//! player to the match
class IAWSGameLiftMatchmakingInternalRequests
{
public:
AZ_RTTI(IAWSGameLiftMatchmakingInternalRequests, "{C2DA440E-74E0-411E-813D-5880B50B0C9E}");
IAWSGameLiftMatchmakingInternalRequests() = default;
virtual ~IAWSGameLiftMatchmakingInternalRequests() = default;
//! StartPolling
//! Request to start process for polling matchmaking ticket based on given ticket id and player id
//! @param ticketId The requested matchmaking ticket id
//! @param playerId The requested matchmaking player id
virtual void StartPolling(const AZStd::string& ticketId, const AZStd::string& playerId) = 0;
//! StopPolling
//! Request to stop process for polling matchmaking ticket
virtual void StopPolling() = 0;
};
} // namespace AWSGameLift
@@ -36,8 +36,6 @@
#include <aws/gamelift/model/StopMatchmakingRequest.h>
#include <aws/gamelift/model/StopMatchmakingResult.h>
#include <Request/IAWSGameLiftMatchmakingInternalRequests.h>
using namespace Aws::GameLift;
class GameLiftClientMock
@@ -32,12 +32,12 @@ set(FILES
Source/Activity/AWSGameLiftLeaveSessionActivity.h
Source/Activity/AWSGameLiftSearchSessionsActivity.cpp
Source/Activity/AWSGameLiftSearchSessionsActivity.h
Source/AWSGameLiftClientLocalTicketTracker.cpp
Source/AWSGameLiftClientLocalTicketTracker.h
Source/Activity/AWSGameLiftStartMatchmakingActivity.cpp
Source/Activity/AWSGameLiftStartMatchmakingActivity.h
Source/Activity/AWSGameLiftStopMatchmakingActivity.cpp
Source/Activity/AWSGameLiftStopMatchmakingActivity.h
Source/AWSGameLiftClientLocalTicketTracker.cpp
Source/AWSGameLiftClientLocalTicketTracker.h
Source/AWSGameLiftClientManager.cpp
Source/AWSGameLiftClientManager.h
Source/AWSGameLiftClientSystemComponent.cpp
@@ -50,5 +50,4 @@ set(FILES
Source/Request/AWSGameLiftStartMatchmakingRequest.cpp
Source/Request/AWSGameLiftStopMatchmakingRequest.cpp
Source/Request/IAWSGameLiftInternalRequests.h
Source/Request/IAWSGameLiftMatchmakingInternalRequests.h
)
@@ -336,14 +336,11 @@ namespace AWSGameLift
BuildServerMatchBackfillPlayerAttributes(
players[playerIndex][AWSGameLiftMatchmakingPlayerAttributesKeyName], outPlayer);
}
}
else
{
return false;
return true;
}
}
}
return true;
return false;
}
void AWSGameLiftServerManager::BuildServerMatchBackfillPlayerAttributes(
@@ -461,12 +458,16 @@ namespace AWSGameLift
AZ_TracePrintf(AWSGameLiftServerManagerName, "Notifying GameLift server process is ending ...");
Aws::GameLift::GenericOutcome processEndingOutcome = m_gameLiftServerSDKWrapper->ProcessEnding();
AZ_TracePrintf(AWSGameLiftServerManagerName, "ProcessEnding request against Amazon GameLift service is complete.");
[[maybe_unused]] bool processEndingIsSuccess = processEndingOutcome.IsSuccess();
AZ_Error(AWSGameLiftServerManagerName, processEndingIsSuccess, AWSGameLiftServerProcessEndingErrorMessage,
processEndingOutcome.GetError().GetErrorMessage().c_str());
if (processEndingOutcome.IsSuccess())
{
AZ_TracePrintf(AWSGameLiftServerManagerName, "ProcessEnding request against Amazon GameLift service succeeded.");
AzFramework::SessionNotificationBus::Broadcast(&AzFramework::SessionNotifications::OnDestroySessionEnd);
}
else
{
AZ_Error(AWSGameLiftServerManagerName, false, AWSGameLiftServerProcessEndingErrorMessage,
processEndingOutcome.GetError().GetErrorMessage().c_str());
}
}
void AWSGameLiftServerManager::HandlePlayerLeaveSession(const AzFramework::PlayerConnectionConfig& playerConnectionConfig)
@@ -546,15 +547,16 @@ namespace AWSGameLift
{
AZ_TracePrintf(AWSGameLiftServerManagerName, "Activating GameLift game session ...");
Aws::GameLift::GenericOutcome activationOutcome = m_gameLiftServerSDKWrapper->ActivateGameSession();
AZ_TracePrintf(AWSGameLiftServerManagerName, "ActivateGameSession request against Amazon GameLift service is complete.");
if (activationOutcome.IsSuccess())
{
AZ_TracePrintf(AWSGameLiftServerManagerName, "ActivateGameSession request against Amazon GameLift service succeeded.");
// Register server manager as handler once game session has been activated
if (!AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Get())
{
AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Register(this);
}
AzFramework::SessionNotificationBus::Broadcast(&AzFramework::SessionNotifications::OnCreateSessionEnd);
}
else
{
@@ -588,17 +590,18 @@ namespace AWSGameLift
void AWSGameLiftServerManager::OnUpdateGameSession(const Aws::GameLift::Server::Model::UpdateGameSession& updateGameSession)
{
AzFramework::SessionConfig sessionConfig = BuildSessionConfig(updateGameSession.GetGameSession());
Aws::GameLift::Server::Model::UpdateReason updateReason = updateGameSession.GetUpdateReason();
AzFramework::SessionNotificationBus::Broadcast(&AzFramework::SessionNotifications::OnUpdateSessionBegin,
sessionConfig, Aws::GameLift::Server::Model::UpdateReasonMapper::GetNameForUpdateReason(updateReason).c_str());
// Update game session data locally
if (updateReason == Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED)
{
UpdateGameSessionData(updateGameSession.GetGameSession());
}
AzFramework::SessionConfig sessionConfig = BuildSessionConfig(updateGameSession.GetGameSession());
AzFramework::SessionNotificationBus::Broadcast(
&AzFramework::SessionNotifications::OnUpdateSessionBegin,
sessionConfig,
Aws::GameLift::Server::Model::UpdateReasonMapper::GetNameForUpdateReason(updateReason).c_str());
AzFramework::SessionNotificationBus::Broadcast(&AzFramework::SessionNotifications::OnUpdateSessionEnd);
}
bool AWSGameLiftServerManager::RemoveConnectedPlayer(uint32_t playerConnectionId, AZStd::string& outPlayerSessionId)
@@ -654,7 +657,7 @@ namespace AWSGameLift
}
else
{
AZ_TracePrintf(AWSGameLiftServerManagerName, "StartMatchBackfill request against Amazon GameLift service is complete.");
AZ_TracePrintf(AWSGameLiftServerManagerName, "StartMatchBackfill request against Amazon GameLift service succeeded.");
return true;
}
}
@@ -686,7 +689,7 @@ namespace AWSGameLift
}
else
{
AZ_TracePrintf(AWSGameLiftServerManagerName, "StopMatchBackfill request against Amazon GameLift service is complete.");
AZ_TracePrintf(AWSGameLiftServerManagerName, "StopMatchBackfill request against Amazon GameLift service succeeded.");
return true;
}
}
@@ -94,7 +94,7 @@ namespace AWSGameLift
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSTypeName[] = "S";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSServerTypeName[] = "STRING";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeNTypeName[] = "N";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeNServerTypeName[] = "NUMBER";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeNServerTypeName[] = "DOUBLE";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSLTypeName[] = "SL";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSLServerTypeName[] = "STRING_LIST";
static constexpr const char AWSGameLiftMatchmakingPlayerAttributeSDMTypeName[] = "SDM";
@@ -34,13 +34,20 @@ R"({
"valueAttribute":"testmode"
},
"level":{
"attributeType":"NUMBER",
"attributeType":"DOUBLE",
"valueAttribute":10.0
},
"items":{
"attributeType":"STRING_LIST",
"valueAttribute":["test1","test2","test3"]
}
}},
{"playerId":"secondplayer",
"attributes":{
"mode":{
"attributeType":"STRING",
"valueAttribute":"testmode"
}
}}
]}
]
@@ -162,8 +169,11 @@ R"({
MOCK_METHOD0(OnSessionHealthCheck, bool());
MOCK_METHOD1(OnCreateSessionBegin, bool(const AzFramework::SessionConfig&));
MOCK_METHOD0(OnCreateSessionEnd, void());
MOCK_METHOD0(OnDestroySessionBegin, bool());
MOCK_METHOD0(OnDestroySessionEnd, void());
MOCK_METHOD2(OnUpdateSessionBegin, void(const AzFramework::SessionConfig&, const AZStd::string&));
MOCK_METHOD0(OnUpdateSessionEnd, void());
};
class GameLiftServerManagerTest
@@ -254,6 +264,7 @@ R"({
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(false));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), GetTerminationTime()).Times(1);
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(0);
EXPECT_CALL(handlerMock, OnDestroySessionEnd()).Times(0);
AZ_TEST_START_TRACE_SUPPRESSION;
m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onProcessTerminateFunc();
@@ -274,13 +285,40 @@ R"({
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), GetTerminationTime()).Times(1);
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1);
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding())
.Times(1)
.WillOnce(testing::Return(Aws::GameLift::GenericOutcome(nullptr)));
EXPECT_CALL(handlerMock, OnDestroySessionEnd()).Times(1);
m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onProcessTerminateFunc();
EXPECT_FALSE(AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Get());
}
TEST_F(GameLiftServerManagerTest, OnProcessTerminate_OnDestroySessionBeginReturnsTrue_TerminationNotificationSentButFail)
{
m_serverManager->InitializeGameLiftServerSDK();
m_serverManager->NotifyGameLiftProcessReady();
if (!AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Get())
{
AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Register(m_serverManager.get());
}
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), GetTerminationTime()).Times(1);
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding())
.Times(1)
.WillOnce(testing::Return(Aws::GameLift::GenericOutcome()));
EXPECT_CALL(handlerMock, OnDestroySessionEnd()).Times(0);
AZ_TEST_START_TRACE_SUPPRESSION;
m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onProcessTerminateFunc();
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
EXPECT_FALSE(AZ::Interface<AzFramework::ISessionHandlingProviderRequests>::Get());
}
TEST_F(GameLiftServerManagerTest, OnHealthCheck_OnSessionHealthCheckReturnsTrue_CallbackFunctionReturnsTrue)
{
m_serverManager->InitializeGameLiftServerSDK();
@@ -316,6 +354,7 @@ R"({
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(false));
EXPECT_CALL(handlerMock, OnCreateSessionEnd()).Times(0);
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ProcessEnding()).Times(1);
AZ_TEST_START_TRACE_SUPPRESSION;
@@ -329,6 +368,7 @@ R"({
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(handlerMock, OnCreateSessionEnd()).Times(1);
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ActivateGameSession())
.Times(1)
@@ -349,6 +389,7 @@ R"({
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnCreateSessionBegin(testing::_)).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(handlerMock, OnCreateSessionEnd()).Times(0);
EXPECT_CALL(handlerMock, OnDestroySessionBegin()).Times(1).WillOnce(testing::Return(true));
EXPECT_CALL(*(m_serverManager->m_gameLiftServerSDKWrapperMockPtr), ActivateGameSession())
.Times(1)
@@ -359,12 +400,13 @@ R"({
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
}
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithUnknownReason_OnUpdateSessionBeginGetCalledOnce)
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithUnknownReason_OnUpdateSessionGetCalledOnce)
{
m_serverManager->InitializeGameLiftServerSDK();
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1);
EXPECT_CALL(handlerMock, OnUpdateSessionEnd()).Times(1);
m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc(
Aws::GameLift::Server::Model::UpdateGameSession(
@@ -373,12 +415,13 @@ R"({
"testticket"));
}
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithEmptyMatchmakingData_OnUpdateSessionBeginGetCalledOnce)
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithEmptyMatchmakingData_OnUpdateSessionGetCalledOnce)
{
m_serverManager->InitializeGameLiftServerSDK();
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1);
EXPECT_CALL(handlerMock, OnUpdateSessionEnd()).Times(1);
m_serverManager->m_gameLiftServerSDKWrapperMockPtr->m_onUpdateGameSessionFunc(
Aws::GameLift::Server::Model::UpdateGameSession(
@@ -387,12 +430,13 @@ R"({
"testticket"));
}
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithValidMatchmakingData_OnUpdateSessionBeginGetCalledOnce)
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithValidMatchmakingData_OnUpdateSessionGetCalledOnce)
{
m_serverManager->InitializeGameLiftServerSDK();
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1);
EXPECT_CALL(handlerMock, OnUpdateSessionEnd()).Times(1);
Aws::GameLift::Server::Model::GameSession gameSession;
gameSession.SetMatchmakerData(TEST_SERVER_MATCHMAKING_DATA);
@@ -401,12 +445,13 @@ R"({
gameSession, Aws::GameLift::Server::Model::UpdateReason::MATCHMAKING_DATA_UPDATED, "testticket"));
}
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithInvalidMatchmakingData_OnUpdateSessionBeginGetCalledOnce)
TEST_F(GameLiftServerManagerTest, OnUpdateGameSession_TriggerWithInvalidMatchmakingData_OnUpdateSessionGetCalledOnce)
{
m_serverManager->InitializeGameLiftServerSDK();
m_serverManager->NotifyGameLiftProcessReady();
SessionNotificationsHandlerMock handlerMock;
EXPECT_CALL(handlerMock, OnUpdateSessionBegin(testing::_, testing::_)).Times(1);
EXPECT_CALL(handlerMock, OnUpdateSessionEnd()).Times(1);
Aws::GameLift::Server::Model::GameSession gameSession;
gameSession.SetMatchmakerData("{invalid}");
@@ -122,14 +122,45 @@ namespace ImageProcessingAtom
{
format = ePixelFormat_BC1;
}
else if (header.ddspf.dwFourCC == FOURCC_DXT5 || header.ddspf.dwFourCC == FOURCC_DXT4)
else if (header.ddspf.dwFourCC == FOURCC_DXT5)
{
format = ePixelFormat_BC3;
}
else
else if (header.ddspf.dwFourCC == FOURCC_3DCP)
{
AZ_Error("Image Processing", false, "unsupported fourCC format: 0x%x", header.ddspf.dwFourCC);
return nullptr;
format = ePixelFormat_BC4;
}
else if (header.ddspf.dwFourCC == FOURCC_3DC)
{
format = ePixelFormat_BC5;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_R32F)
{
format = ePixelFormat_R32F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_G32R32F)
{
format = ePixelFormat_R32G32F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_A32B32G32R32F)
{
format = ePixelFormat_R32G32B32A32F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_R16F)
{
format = ePixelFormat_R16F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_G16R16F)
{
format = ePixelFormat_R16G16F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_A16B16G16R16F)
{
format = ePixelFormat_R16G16B16A16F;
}
else if (header.ddspf.dwFourCC == DDS_FOURCC_A16B16G16R16)
{
format = ePixelFormat_R16G16B16A16;
}
}
else
@@ -260,4 +260,6 @@ namespace ImageProcessingAtom
const static AZ::u32 FOURCC_DXT3 = IMAGE_BUIDER_MAKEFOURCC('D', 'X', 'T', '3');
const static AZ::u32 FOURCC_DXT4 = IMAGE_BUIDER_MAKEFOURCC('D', 'X', 'T', '4');
const static AZ::u32 FOURCC_DXT5 = IMAGE_BUIDER_MAKEFOURCC('D', 'X', 'T', '5');
const static AZ::u32 FOURCC_3DCP = IMAGE_BUIDER_MAKEFOURCC('A', 'T', 'I', '1');
const static AZ::u32 FOURCC_3DC = IMAGE_BUIDER_MAKEFOURCC('A', 'T', 'I', '2');
}
@@ -73,7 +73,7 @@
},
{
"Name": "m_colorGradingPreSaturation",
"Value": 1.0 // -100 ... 100
"Value": 1.0 // 0 ... 2
},
{
"Name": "m_colorFilterIntensity",
@@ -101,7 +101,7 @@
},
{
"Name": "m_colorGradingPostSaturation",
"Value": 1.0 // -100 ... 100
"Value": 1.0 // 0 ... 2
},
{
"Name": "m_smhShadowsStart",
@@ -105,6 +105,11 @@
}
]
},
{
"Name": "LutGenerationPass",
"TemplateName": "LutGenerationTemplate",
"Enabled": true
},
{
"Name": "LookModificationTransformPass",
"TemplateName": "LookModificationTransformTemplate",
@@ -0,0 +1,47 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "PassAsset",
"ClassData": {
"PassTemplate": {
"Name": "LutGenerationTemplate",
"PassClass": "LutGenerationPass",
"Slots": [
{
"Name": "LutOutput",
"SlotType": "Output",
"ScopeAttachmentUsage": "RenderTarget",
"LoadStoreAction": {
"LoadAction": "DontCare"
}
}
],
"ImageAttachments": [
{
"Name": "ColorGradingLut",
"ImageDescriptor": {
"Format": "R32G32B32A32_FLOAT",
"BindFlags": [
"ShaderWrite"
]
}
}
],
"Connections": [
{
"LocalSlot": "LutOutput",
"AttachmentRef": {
"Pass": "This",
"Attachment": "ColorGradingLut"
}
}
],
"PassData": {
"$type": "FullscreenTrianglePassData",
"ShaderAsset": {
"FilePath": "Shaders/ColorGrading/LutGeneration.shader"
}
}
}
}
}
@@ -511,7 +511,11 @@
{
"Name": "HDRColorGradingTemplate",
"Path": "Passes/HDRColorGrading.pass"
}
},
{
"Name": "LutGenerationTemplate",
"Path": "Passes/LutGeneration.pass"
}
]
}
}
@@ -59,7 +59,7 @@ UNDISCLOSED.
#pragma once
static const float HALF_MAX = 65504.0f;
#include <Atom/Features/PostProcessing/Aces.azsli>
float AcesCcToLinear(float value)
{
@@ -0,0 +1,154 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <Atom/RPI/Math.azsli>
#include <Atom/Features/ColorManagement/TransformColor.azsli>
#include <Atom/Features/PostProcessing/AcesColorSpaceConversion.azsli>
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli>
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli>
#include <3rdParty/Features/PostProcessing/KelvinToRgb.azsli>
static const float FloatEpsilon = 1.192092896e-07; // 1.0 + FloatEpsilon != 1.0, smallest positive float
static const float FloatMin = FLOAT_32_MIN; // Min float number that is positive
static const float FloatMax = FLOAT_32_MAX; // Max float number representable
static const float AcesCcMidGrey = 0.4135884;
float SaturateWithEpsilon(float value)
{
return clamp(value, FloatEpsilon, 1.0f);
}
// Below are the color grading functions. These expect the frame color to be in ACEScg space.
// Note that some functions may have some quirks in their implementation and is subject to change.
float3 ColorGradePostExposure (float3 frameColor, float exposure)
{
frameColor *= pow(2.0f, exposure);
return frameColor;
}
// The contrast equation is performed in ACEScc (logarithmic) color space.
float3 ColorGradingContrast (float3 frameColor, float midgrey, float amount)
{
const float contrastAdjustment = amount * 0.01f + 1.0f;
frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScg, ColorSpaceId::ACEScc);
frameColor = (frameColor - midgrey) * contrastAdjustment + midgrey;
return frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScc, ColorSpaceId::ACEScg);
}
// The swatchColor param expects a linear RGB value.
float3 ColorGradeColorFilter (float3 frameColor, float3 swatchColor, float alpha, float colorFilterIntensity)
{
swatchColor = TransformColor(swatchColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
swatchColor *= pow(2.0f, colorFilterIntensity);
const float3 frameAdjust = frameColor * swatchColor;
return frameColor = lerp(frameColor, frameAdjust, alpha);
}
float3 ColorGradeHueShift (float3 frameColor, float amount)
{
float3 frameHsv = RgbToHsv(frameColor);
const float hue = frameHsv.x + amount;
frameHsv.x = RotateHue(hue, 0.0, 1.0);
return HsvToRgb(frameHsv);
}
float3 ColorGradeSaturation (float3 frameColor, float control)
{
const float vLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
return (frameColor - vLuminance) * control + vLuminance;
}
float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin)
{
const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL
return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance
}
// pow(f, e) won't work if f is negative, or may cause inf/NAN.
float3 NoNanPow(float3 base, float3 power)
{
return pow(max(abs(base), float3(FloatEpsilon, FloatEpsilon, FloatEpsilon)), power);
}
float3 ColorGradeSplitTone (
float3 frameColor,
float balance,
float weight,
float3 splitToneShadowsColor,
float3 splitToneHighlightsColor)
{
float3 frameSplitTone = NoNanPow(frameColor, 1.0 / 2.2);
const float t = SaturateWithEpsilon(CalculateLuminance(SaturateWithEpsilon(frameSplitTone), ColorSpaceId::ACEScg) + balance);
const float3 shadows = lerp(0.5, splitToneShadowsColor, 1.0 - t);
const float3 highlights = lerp(0.5, splitToneHighlightsColor, t);
frameSplitTone = BlendMode_SoftLight(frameSplitTone, shadows);
frameSplitTone = BlendMode_SoftLight(frameSplitTone, highlights);
frameSplitTone = NoNanPow(frameSplitTone, 2.2);
return lerp(frameColor.rgb, frameSplitTone.rgb, weight);
}
float3 ColorGradeChannelMixer (
float3 frameColor,
float3 channelMixingRed,
float3 channelMixingGreen,
float3 channelMixingBlue)
{
return mul(float3x3(channelMixingRed,
channelMixingGreen,
channelMixingBlue),
frameColor);
}
float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStart, float shadowsEnd,
float highlightsStart, float highlightsEnd, float weight,
float4 shadowsColor, float4 midtonesColor, float4 highlightsColor)
{
const float3 shadowsColorACEScg = TransformColor(shadowsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float3 midtonesColorACEScg = TransformColor(midtonesColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float3 highlightsColorACEScg = TransformColor(highlightsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float cLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
const float shadowsWeight = 1.0 - smoothstep(shadowsStart, shadowsEnd, cLuminance);
const float highlightsWeight = smoothstep(highlightsStart, highlightsEnd, cLuminance);
const float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight;
const float3 frameSmh = frameColor * shadowsColorACEScg * shadowsWeight +
frameColor * midtonesColorACEScg * midtonesWeight +
frameColor * highlightsColorACEScg * highlightsWeight;
return lerp(frameColor.rgb, frameSmh.rgb, weight);
}
// perform color grading in ACEScg space
float3 ColorGrade(float3 frameColor)
{
frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin), PassSrg::m_whiteBalanceWeight);
frameColor = lerp(frameColor, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb,
PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight);
frameColor = max(frameColor, 0.0);
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight);
frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight,
PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor);
frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue);
frameColor = max(frameColor, 0.0);
frameColor = ColorGradeShadowsMidtonesHighlights(frameColor, PassSrg::m_smhShadowsStart, PassSrg::m_smhShadowsEnd,
PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight,
PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor);
frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight);
return max(frameColor.rgb, 0.0);
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <Atom/RPI/Math.azsli>
#include <Atom/Features/SrgSemantics.azsli>
#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
#include <Atom/Features/PostProcessing/Aces.azsli>
#include <Atom/Features/PostProcessing/Shapers.azsli>
float3 convert2Dto3DLutCoords(float2 uv, float width, float height)
{
// convert from center pixel uvs to [0,1]
float offset = 1.0/height/2.0;
float scale = 1.0 - offset*2.0;
float2 adjustedUv = float2(uv.x * width, uv.y * height);
float3 coords = float3(adjustedUv.x%height, 0.5 + int(adjustedUv.x/height), adjustedUv.y)/height;
return (coords - offset)/scale;
}
ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
{
// framebuffer sampler
Sampler PointSampler
{
MinFilter = Point;
MagFilter = Point;
MipFilter = Point;
AddressU = Clamp;
AddressV = Clamp;
AddressW = Clamp;
};
int m_lutResolution;
int m_shaperType;
float m_shaperBias;
float m_shaperScale;
float m_colorAdjustmentWeight;
float m_colorGradingExposure;
float m_colorGradingContrast;
float m_colorGradingPreSaturation;
float m_colorFilterIntensity;
float m_colorFilterMultiply;
float4 m_colorFilterSwatch;
float m_whiteBalanceWeight;
float m_whiteBalanceKelvin;
float m_whiteBalanceTint;
float m_splitToneBalance;
float m_splitToneWeight;
float4 m_splitToneShadowsColor;
float4 m_splitToneHighlightsColor;
float m_smhShadowsStart;
float m_smhShadowsEnd;
float m_smhHighlightsStart;
float m_smhHighlightsEnd;
float m_smhWeight;
float4 m_smhShadowsColor;
float4 m_smhMidtonesColor;
float4 m_smhHighlightsColor;
float3 m_channelMixingRed;
float3 m_channelMixingGreen;
float3 m_channelMixingBlue;
float m_finalAdjustmentWeight;
float m_colorGradingPostSaturation;
float m_colorGradingHueShift;
}
#include <Atom/Features/PostProcessing/HDRColorGradingCommon.azsl>
struct PSOutput
{
float4 m_lutOutput : SV_Target0;
};
PSOutput MainPS(VSOutput IN)
{
ShaperType shaperType = (ShaperType)PassSrg::m_shaperType;
int lutResolution = PassSrg::m_lutResolution;
PSOutput OUT;
// baseCoords are from 0-1
float3 baseCoords = convert2Dto3DLutCoords(IN.m_texCoord, lutResolution*lutResolution, lutResolution);
float3 linearColor = ShaperToLinear(baseCoords, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
linearColor = TransformColor(linearColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
float3 gradedColor = ColorGrade(linearColor);
gradedColor = TransformColor(gradedColor, ColorSpaceId::ACEScg, ColorSpaceId::LinearSRGB);
// Bring back coordinates into 0-1
float3 shapedColor = LinearToShaper(gradedColor, shaperType, PassSrg::m_shaperBias, PassSrg::m_shaperScale);
shapedColor = saturate(shapedColor);
OUT.m_lutOutput = float4(shapedColor, 1.0);
return OUT;
}
@@ -0,0 +1,22 @@
{
"Source" : "LutGeneration",
"DepthStencilState" : {
"Depth" : { "Enable" : false }
},
"ProgramSettings":
{
"EntryPoints":
[
{
"name": "MainVS",
"type": "Vertex"
},
{
"name": "MainPS",
"type": "Fragment"
}
]
}
}
@@ -13,18 +13,6 @@
#include <Atom/Features/PostProcessing/FullscreenPixelInfo.azsli>
#include <Atom/Features/PostProcessing/FullscreenVertex.azsli>
#include <Atom/Features/ColorManagement/TransformColor.azsli>
#include <Atom/Features/PostProcessing/AcesColorSpaceConversion.azsli>
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_Separable.azsli>
#include <3rdParty/Features/PostProcessing/PSstyleColorBlends_NonSeparable.azsli>
#include <3rdParty/Features/PostProcessing/KelvinToRgb.azsli>
static const float FloatEpsilon = 1.192092896e-07; // 1.0 + FloatEpsilon != 1.0, smallest positive float
static const float FloatMin = FLOAT_32_MIN; // Min float number that is positive
static const float FloatMax = FLOAT_32_MAX; // Max float number representable
static const float AcesCcMidGrey = 0.4135884;
ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
{
// get the framebuffer
@@ -41,153 +29,42 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
AddressW = Clamp;
};
float m_colorAdjustmentWeight;
float m_colorGradingExposure;
float m_colorGradingContrast;
float m_colorGradingHueShift;
float m_colorGradingPreSaturation;
float m_colorFilterIntensity;
float m_colorFilterMultiply;
float4 m_colorFilterSwatch;
float m_whiteBalanceWeight;
float m_whiteBalanceKelvin;
float m_whiteBalanceTint;
float m_splitToneBalance;
float m_splitToneWeight;
float m_colorGradingPostSaturation;
float4 m_splitToneShadowsColor;
float4 m_splitToneHighlightsColor;
float m_smhShadowsStart;
float m_smhShadowsEnd;
float m_smhHighlightsStart;
float m_smhHighlightsEnd;
float m_smhWeight;
float4 m_smhShadowsColor;
float4 m_smhMidtonesColor;
float4 m_smhHighlightsColor;
float3 m_channelMixingRed;
float3 m_channelMixingGreen;
float3 m_channelMixingBlue;
float4 m_colorFilterSwatch;
float4 m_splitToneShadowsColor;
float4 m_splitToneHighlightsColor;
float4 m_smhShadowsColor;
float4 m_smhMidtonesColor;
float4 m_smhHighlightsColor;
float m_finalAdjustmentWeight;
float m_colorGradingPostSaturation;
float m_colorGradingHueShift;
}
float SaturateWithEpsilon(float value)
{
return clamp(value, FloatEpsilon, 1.0f);
}
// Below are the color grading functions. These expect the frame color to be in ACEScg space.
// Note that some functions may have some quirks in their implementation and is subject to change.
float3 ColorGradePostExposure (float3 frameColor, float exposure)
{
frameColor *= pow(2.0f, exposure);
return frameColor;
}
// The contrast equation is performed in ACEScc (logarithmic) color space.
float3 ColorGradingContrast (float3 frameColor, float midgrey, float amount)
{
const float contrastAdjustment = amount * 0.01f + 1.0f;
frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScg, ColorSpaceId::ACEScc);
frameColor = (frameColor - midgrey) * contrastAdjustment + midgrey;
return frameColor = TransformColor(frameColor.rgb, ColorSpaceId::ACEScc, ColorSpaceId::ACEScg);
}
// The swatchColor param expects a linear RGB value.
float3 ColorGradeColorFilter (float3 frameColor, float3 swatchColor, float alpha)
{
swatchColor = TransformColor(swatchColor, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
swatchColor *= pow(2.0f, PassSrg::m_colorFilterIntensity);
const float3 frameAdjust = frameColor * swatchColor;
return frameColor = lerp(frameColor, frameAdjust, alpha);
}
float3 ColorGradeHueShift (float3 frameColor, float amount)
{
float3 frameHsv = RgbToHsv(frameColor);
const float hue = frameHsv.x + amount;
frameHsv.x = RotateHue(hue, 0.0, 1.0);
return HsvToRgb(frameHsv);
}
float3 ColorGradeSaturation (float3 frameColor, float control)
{
const float vLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
return (frameColor - vLuminance) * control + vLuminance;
}
float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin)
{
const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL
return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance
}
// pow(f, e) won't work if f is negative, or may cause inf/NAN.
float3 NoNanPow(float3 base, float3 power)
{
return pow(max(abs(base), float3(FloatEpsilon, FloatEpsilon, FloatEpsilon)), power);
}
float3 ColorGradeSplitTone (float3 frameColor, float balance, float weight)
{
float3 frameSplitTone = NoNanPow(frameColor, 1.0 / 2.2);
const float t = SaturateWithEpsilon(CalculateLuminance(SaturateWithEpsilon(frameSplitTone), ColorSpaceId::ACEScg) + balance);
const float3 shadows = lerp(0.5, PassSrg::m_splitToneShadowsColor.rgb, 1.0 - t);
const float3 highlights = lerp(0.5, PassSrg::m_splitToneHighlightsColor.rgb, t);
frameSplitTone = BlendMode_SoftLight(frameSplitTone, shadows);
frameSplitTone = BlendMode_SoftLight(frameSplitTone, highlights);
frameSplitTone = NoNanPow(frameSplitTone, 2.2);
return lerp(frameColor.rgb, frameSplitTone.rgb, weight);
}
float3 ColorGradeChannelMixer (float3 frameColor)
{
return mul(float3x3(PassSrg::m_channelMixingRed.rgb,
PassSrg::m_channelMixingGreen.rgb,
PassSrg::m_channelMixingBlue.rgb),
frameColor);
}
float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStart, float shadowsEnd,
float highlightsStart, float highlightsEnd, float weight,
float4 shadowsColor, float4 midtonesColor, float4 highlightsColor)
{
const float3 shadowsColorACEScg = TransformColor(shadowsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float3 midtonesColorACEScg = TransformColor(midtonesColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float3 highlightsColorACEScg = TransformColor(highlightsColor.rgb, ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float cLuminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
const float shadowsWeight = 1.0 - smoothstep(shadowsStart, shadowsEnd, cLuminance);
const float highlightsWeight = smoothstep(highlightsStart, highlightsEnd, cLuminance);
const float midtonesWeight = 1.0 - shadowsWeight - highlightsWeight;
const float3 frameSmh = frameColor * shadowsColorACEScg * shadowsWeight +
frameColor * midtonesColorACEScg * midtonesWeight +
frameColor * highlightsColorACEScg * highlightsWeight;
return lerp(frameColor.rgb, frameSmh.rgb, weight);
}
float3 ColorGrade (float3 frameColor)
{
frameColor = ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure);
frameColor = ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin);
frameColor = ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast);
frameColor = ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb,
PassSrg::m_colorFilterMultiply);
frameColor = max(frameColor, 0.0);
frameColor = ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation);
frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight);
frameColor = ColorGradeChannelMixer(frameColor);
frameColor = max(frameColor, 0.0);
frameColor = ColorGradeShadowsMidtonesHighlights(frameColor, PassSrg::m_smhShadowsStart, PassSrg::m_smhShadowsEnd,
PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight,
PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor);
frameColor = ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift);
frameColor = ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation);
return frameColor.rgb;
}
#include <Atom/Features/PostProcessing/HDRColorGradingCommon.azsl>
PSOutput MainPS(VSOutput IN)
{
@@ -86,7 +86,6 @@ set(FILES
Passes/CascadedShadowmaps.pass
Passes/CheckerboardResolveColor.pass
Passes/CheckerboardResolveDepth.pass
Passes/HDRColorGrading.pass
Passes/ContrastAdaptiveSharpening.pass
Passes/ConvertToAcescg.pass
Passes/DebugOverlayParent.pass
@@ -144,6 +143,7 @@ set(FILES
Passes/ForwardSubsurfaceMSAA.pass
Passes/FullscreenCopy.pass
Passes/FullscreenOutputOnly.pass
Passes/HDRColorGrading.pass
Passes/ImGui.pass
Passes/KawaseShadowBlur.pass
Passes/LightAdaptationParent.pass
@@ -158,6 +158,7 @@ set(FILES
Passes/LowEndPipeline.pass
Passes/LuminanceHeatmap.pass
Passes/LuminanceHistogramGenerator.pass
Passes/LutGeneration.pass
Passes/MainPipeline.pass
Passes/MainPipelineRenderToTexture.pass
Passes/ThumbnailPipeline.pass
@@ -281,6 +282,7 @@ set(FILES
ShaderLib/Atom/Features/PostProcessing/FullscreenVertexUtil.azsli
ShaderLib/Atom/Features/PostProcessing/GlyphData.azsli
ShaderLib/Atom/Features/PostProcessing/GlyphRender.azsli
ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl
ShaderLib/Atom/Features/PostProcessing/PostProcessUtil.azsli
ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli
ShaderLib/Atom/Features/ScreenSpace/ScreenSpaceUtil.azsli
@@ -315,6 +317,8 @@ set(FILES
Shaders/BRDFTexture/BRDFTextureCS.shader
Shaders/Checkerboard/CheckerboardColorResolveCS.azsl
Shaders/Checkerboard/CheckerboardColorResolveCS.shader
Shaders/ColorGrading/LutGeneration.azsl
Shaders/ColorGrading/LutGeneration.shader
Shaders/Depth/DepthPass.azsl
Shaders/Depth/DepthPass.shader
Shaders/Depth/DepthPassTransparentMax.shader
@@ -39,6 +39,7 @@ ly_add_target(
Gem::Atom_Utils.Static
Gem::Atom_Feature_Common.Public
Gem::ImGui.imguilib
3rdParty::TIFF
#3rdParty::lux_core # AZ_TRAIT_LUXCORE_SUPPORTED is disabled in every platform, Issue #3915 will remove
RUNTIME_DEPENDENCIES
Gem::ImGui.imguilib
@@ -75,6 +76,7 @@ ly_add_target(
FILES_CMAKE
atom_feature_common_shared_files.cmake
../Assets/atom_feature_common_asset_files.cmake
../Editor/atom_feature_common_editor_script_files.cmake
PLATFORM_INCLUDE_FILES
${pal_source_dir}/runtime_dependencies_clients.cmake
INCLUDE_DIRECTORIES
@@ -0,0 +1,22 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
namespace AZ
{
namespace Render
{
enum class LutResolution
{
Lut16x16x16 = 16,
Lut32x32x32 = 32,
Lut64x64x64 = 64
};
}
}
@@ -10,28 +10,42 @@
// PARAM(NAME, MEMBER_NAME, DEFAULT_VALUE, ...)
AZ_GFX_BOOL_PARAM(Enabled, m_enabled, false)
AZ_GFX_BOOL_PARAM(GenerateLut, m_generateLut, false)
AZ_GFX_FLOAT_PARAM(ColorAdjustmentWeight, m_colorAdjustmentWeight, 1.0)
AZ_GFX_FLOAT_PARAM(ColorGradingExposure, m_colorGradingExposure, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingContrast, m_colorGradingContrast, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingHueShift, m_colorGradingHueShift, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingPreSaturation, m_colorGradingPreSaturation, 1.0)
AZ_GFX_FLOAT_PARAM(ColorGradingPreSaturation, m_colorGradingPreSaturation, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingFilterIntensity, m_colorGradingFilterIntensity, 1.0)
AZ_GFX_FLOAT_PARAM(ColorGradingFilterMultiply, m_colorGradingFilterMultiply, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingPostSaturation, m_colorGradingPostSaturation, 1.0)
AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f, 0.5f))
AZ_GFX_FLOAT_PARAM(WhiteBalanceWeight, m_whiteBalanceWeight, 0.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceTint, m_whiteBalanceTint, 0.0)
AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0)
AZ_GFX_FLOAT_PARAM(SplitToneWeight, m_splitToneWeight, 0.0)
AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0)
AZ_GFX_VEC3_PARAM(SplitToneShadowsColor, m_splitToneShadowsColor, AZ::Vector3(1.0f, 0.5f, 0.5f))
AZ_GFX_VEC3_PARAM(SplitToneHighlightsColor, m_splitToneHighlightsColor, AZ::Vector3(0.1f, 1.0f, 0.1f))
AZ_GFX_FLOAT_PARAM(SmhWeight, m_smhWeight, 0.0)
AZ_GFX_FLOAT_PARAM(SmhShadowsStart, m_smhShadowsStart, 0.0)
AZ_GFX_FLOAT_PARAM(SmhShadowsEnd, m_smhShadowsEnd, 0.3)
AZ_GFX_FLOAT_PARAM(SmhHighlightsStart, m_smhHighlightsStart, 0.55)
AZ_GFX_FLOAT_PARAM(SmhHighlightsEnd, m_smhHighlightsEnd, 1.0)
AZ_GFX_FLOAT_PARAM(SmhWeight, m_smhWeight, 0.0)
AZ_GFX_VEC3_PARAM(ChannelMixingRed, m_channelMixingRed, AZ::Vector3(1.0f, 0.0f, 0.0f))
AZ_GFX_VEC3_PARAM(ChannelMixingGreen, m_channelMixingGreen, AZ::Vector3(0.0f, 1.0f, 0.0f))
AZ_GFX_VEC3_PARAM(ChannelMixingBlue, m_channelMixingBlue, AZ::Vector3(0.0f, 0.f, 1.0f))
AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f, 0.5f))
AZ_GFX_VEC3_PARAM(SplitToneShadowsColor, m_splitToneShadowsColor, AZ::Vector3(1.0f, 0.5f, 0.5f))
AZ_GFX_VEC3_PARAM(SplitToneHighlightsColor, m_splitToneHighlightsColor, AZ::Vector3(0.1f, 1.0f, 0.1f))
AZ_GFX_VEC3_PARAM(SmhShadowsColor, m_smhShadowsColor, AZ::Vector3(1.0f, 0.25f, 0.25f))
AZ_GFX_VEC3_PARAM(SmhMidtonesColor, m_smhMidtonesColor, AZ::Vector3(0.1f, 0.1f, 1.0f))
AZ_GFX_VEC3_PARAM(SmhHighlightsColor, m_smhHighlightsColor, AZ::Vector3(1.0f, 0.0f, 1.0f))
AZ_GFX_VEC3_PARAM(ChannelMixingRed, m_channelMixingRed, AZ::Vector3(1.0f, 0.0f, 0.0f))
AZ_GFX_VEC3_PARAM(ChannelMixingGreen, m_channelMixingGreen, AZ::Vector3(0.0f, 1.0f, 0.0f))
AZ_GFX_VEC3_PARAM(ChannelMixingBlue, m_channelMixingBlue, AZ::Vector3(0.0f, 0.f, 1.0f))
AZ_GFX_COMMON_PARAM(AZ::Render::LutResolution, LutResolution, m_lutResolution, AZ::Render::LutResolution::Lut16x16x16)
AZ_GFX_COMMON_PARAM(AZ::Render::ShaperPresetType, ShaperPresetType, m_shaperPresetType, AZ::Render::ShaperPresetType::None)
AZ_GFX_COMMON_PARAM(float, CustomMinExposure, m_customMinExposure, -6.5)
AZ_GFX_COMMON_PARAM(float, CustomMaxExposure, m_customMaxExposure, 6.5)
AZ_GFX_FLOAT_PARAM(FinalAdjustmentWeight, m_finalAdjustmentWeight, 1.0)
AZ_GFX_FLOAT_PARAM(ColorGradingPostSaturation, m_colorGradingPostSaturation, 0.0)
AZ_GFX_FLOAT_PARAM(ColorGradingHueShift, m_colorGradingHueShift, 0.0)
@@ -12,6 +12,8 @@
#include <AzCore/Math/Vector4.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/Component/EntityId.h>
#include <Atom/Feature/ColorGrading/LutResolution.h>
#include <ACES/Aces.h>
namespace AZ
{
@@ -0,0 +1,90 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ColorGrading/LutGenerationPass.h>
#include <Atom/Feature/ACES/AcesDisplayMapperFeatureProcessor.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
namespace AZ
{
namespace Render
{
RPI::Ptr<LutGenerationPass> LutGenerationPass::Create(const RPI::PassDescriptor& descriptor)
{
RPI::Ptr<LutGenerationPass> pass = aznew LutGenerationPass(descriptor);
return AZStd::move(pass);
}
LutGenerationPass::LutGenerationPass(const RPI::PassDescriptor& descriptor)
: HDRColorGradingPass(descriptor)
{
}
void LutGenerationPass::InitializeInternal()
{
HDRColorGradingPass::InitializeInternal();
m_lutResolutionIndex.Reset();
m_lutShaperTypeIndex.Reset();
m_lutShaperScaleIndex.Reset();
}
void LutGenerationPass::FrameBeginInternal(FramePrepareParams params)
{
const auto* colorGradingSettings = GetHDRColorGradingSettings();
if (colorGradingSettings)
{
m_shaderResourceGroup->SetConstant(m_lutResolutionIndex, colorGradingSettings->GetLutResolution());
auto shaperParams = AcesDisplayMapperFeatureProcessor::GetShaperParameters(
colorGradingSettings->GetShaperPresetType(),
colorGradingSettings->GetCustomMinExposure(),
colorGradingSettings->GetCustomMaxExposure());
m_shaderResourceGroup->SetConstant(m_lutShaperTypeIndex, shaperParams.m_type);
m_shaderResourceGroup->SetConstant(m_lutShaperBiasIndex, shaperParams.m_bias);
m_shaderResourceGroup->SetConstant(m_lutShaperScaleIndex, shaperParams.m_scale);
}
HDRColorGradingPass::FrameBeginInternal(params);
}
void LutGenerationPass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
{
const auto* colorGradingSettings = GetHDRColorGradingSettings();
if (colorGradingSettings)
{
uint32_t lutResolution = aznumeric_cast<uint32_t>(colorGradingSettings->GetLutResolution());
RHI::Size lutSize{ lutResolution * lutResolution, lutResolution, 1 };
RPI::Ptr<RPI::PassAttachment> attachment = FindOwnedAttachment(Name{ "ColorGradingLut" });
RHI::ImageDescriptor& imageDescriptor = attachment->m_descriptor.m_image;
imageDescriptor.m_size = lutSize;
SetViewportScissorFromImageSize(lutSize);
}
HDRColorGradingPass::BuildCommandListInternal(context);
}
bool LutGenerationPass::IsEnabled() const
{
const auto* colorGradingSettings = GetHDRColorGradingSettings();
return colorGradingSettings ? colorGradingSettings->GetGenerateLut() : false;
}
void LutGenerationPass::SetViewportScissorFromImageSize(const RHI::Size& imageSize)
{
const RHI::Viewport viewport(0.f, imageSize.m_width * 1.f, 0.f, imageSize.m_height * 1.f);
const RHI::Scissor scissor(0, 0, imageSize.m_width, imageSize.m_height);
m_viewportState = viewport;
m_scissorState = scissor;
}
} // namespace Render
} // namespace AZ
@@ -0,0 +1,54 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <PostProcessing/HDRColorGradingPass.h>
#include <Atom/RPI.Public/Shader/Shader.h>
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
#include <Atom/RPI.Reflect/Shader/ShaderVariantKey.h>
#include <PostProcess/ColorGrading/HDRColorGradingSettings.h>
#include <Atom/Feature/DisplayMapper/DisplayMapperFeatureProcessorInterface.h>
namespace AZ
{
namespace Render
{
// Performs color grading on an identity LUT strip
class LutGenerationPass
: public AZ::Render::HDRColorGradingPass
{
public:
AZ_RTTI(LutGenerationPass, "{C21DABA8-B538-4C80-BA18-5B97CC9259E5}", AZ::RPI::FullscreenTrianglePass);
AZ_CLASS_ALLOCATOR(LutGenerationPass, SystemAllocator, 0);
virtual ~LutGenerationPass() = default;
//! Creates a ColorGradingPass
static RPI::Ptr<LutGenerationPass> Create(const RPI::PassDescriptor& descriptor);
protected:
LutGenerationPass(const RPI::PassDescriptor& descriptor);
void InitializeInternal() override;
void FrameBeginInternal(FramePrepareParams params) override;
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
bool IsEnabled() const override;
private:
// Set viewport scissor based on output LUT resolution
void SetViewportScissorFromImageSize(const RHI::Size& imageSize);
RHI::ShaderInputNameIndex m_lutResolutionIndex = "m_lutResolution";
RHI::ShaderInputNameIndex m_lutShaperTypeIndex = "m_shaperType";
RHI::ShaderInputNameIndex m_lutShaperBiasIndex = "m_shaperBias";
RHI::ShaderInputNameIndex m_lutShaperScaleIndex = "m_shaperScale";
};
} // namespace Render
} // namespace AZ
@@ -39,6 +39,7 @@
#include <Atom/Feature/AuxGeom/AuxGeomFeatureProcessor.h>
#include <Atom/Feature/Utils/LightingPreset.h>
#include <Atom/Feature/Utils/ModelPreset.h>
#include <ColorGrading/LutGenerationPass.h>
#include <PostProcess/PostProcessFeatureProcessor.h>
#include <PostProcessing/BlendColorGradingLutsPass.h>
#include <PostProcessing/BloomParentPass.h>
@@ -230,6 +231,7 @@ namespace AZ
passSystem->AddPassCreator(Name("HDRColorGradingPass"), &HDRColorGradingPass::Create);
passSystem->AddPassCreator(Name("LookModificationCompositePass"), &LookModificationCompositePass::Create);
passSystem->AddPassCreator(Name("LookModificationTransformPass"), &LookModificationPass::Create);
passSystem->AddPassCreator(Name("LutGenerationPass"), &LutGenerationPass::Create);
passSystem->AddPassCreator(Name("SMAAEdgeDetectionPass"), &SMAAEdgeDetectionPass::Create);
passSystem->AddPassCreator(Name("SMAABlendingWeightCalculationPass"), &SMAABlendingWeightCalculationPass::Create);
passSystem->AddPassCreator(Name("SMAANeighborhoodBlendingPass"), &SMAANeighborhoodBlendingPass::Create);
@@ -35,6 +35,7 @@
#include <AzCore/Preprocessor/EnumReflectUtils.h>
#include <AzCore/Console/Console.h>
#include <tiffio.h>
namespace AZ
{
namespace Render
@@ -110,6 +111,48 @@ namespace AZ
return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to '" + outputFilePath + "'"};
}
FrameCaptureOutputResult TiffFrameCaptureOutput(
const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
{
AZStd::shared_ptr<AZStd::vector<uint8_t>> buffer = readbackResult.m_dataBuffer;
const uint32_t width = readbackResult.m_imageDescriptor.m_size.m_width;
const uint32_t height = readbackResult.m_imageDescriptor.m_size.m_height;
const uint32_t numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format);
const uint32_t bytesPerChannel = AZ::RHI::GetFormatSize(readbackResult.m_imageDescriptor.m_format) / numChannels;
const uint32_t bitsPerChannel = bytesPerChannel * 8;
TIFF* out = TIFFOpen(outputFilePath.c_str(), "w");
TIFFSetField(out, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(out, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, numChannels);
TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, bitsPerChannel);
TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); // interpret each pixel as a float
size_t pitch = width * numChannels * bytesPerChannel;
AZ_Assert((pitch * height) == buffer->size(), "Image buffer does not match allocated bytes for tiff saving.")
unsigned char* raster = (unsigned char*)_TIFFmalloc((tsize_t)(pitch * height));
memcpy(raster, buffer->data(), pitch * height);
bool success = true;
for (uint32_t h = 0; h < height; ++h)
{
size_t offset = h * pitch;
int err = TIFFWriteScanline(out, raster + offset, h, 0);
if (err < 0)
{
success = false;
break;
}
}
_TIFFfree(raster);
TIFFClose(out);
return success ? FrameCaptureOutputResult{ FrameCaptureResult::Success, AZStd::nullopt }
: FrameCaptureOutputResult{ FrameCaptureResult::InternalError, "Unable to save tif frame capture output to " + outputFilePath };
}
FrameCaptureOutputResult DdsFrameCaptureOutput(
const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
{
@@ -492,6 +535,12 @@ namespace AZ
m_result = ddsFrameCapture.m_result;
m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or("");
}
else if (extension == "tiff" || extension == "tif")
{
const auto tifFrameCapture = TiffFrameCaptureOutput(m_outputFilePath, readbackResult);
m_result = tifFrameCapture.m_result;
m_latestCaptureInfo = tifFrameCapture.m_errorMessage.value_or("");
}
else if (extension == "png")
{
if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
@@ -33,6 +33,7 @@ namespace AZ
target->m_enabled = m_enabled;
#define AZ_GFX_BOOL_PARAM(NAME, MEMBER_NAME, DefaultValue) ;
#define AZ_GFX_COMMON_PARAM(ValueType, Name, MemberName, DefaultValue) ;
#define AZ_GFX_FLOAT_PARAM(NAME, MEMBER_NAME, DefaultValue) \
{ \
target->Set##NAME(AZ::Lerp(target->MEMBER_NAME, MEMBER_NAME, alpha)); \
@@ -13,6 +13,7 @@
#include <Atom/Feature/PostProcess/ColorGrading/HDRColorGradingSettingsInterface.h>
#include <PostProcess/PostProcessBase.h>
#include <ACES/Aces.h>
namespace AZ
{
@@ -32,33 +32,39 @@
{
FullscreenTrianglePass::InitializeInternal();
m_colorAdjustmentWeightIndex.Reset();
m_colorGradingExposureIndex.Reset();
m_colorGradingContrastIndex.Reset();
m_colorGradingHueShiftIndex.Reset();
m_colorGradingPreSaturationIndex.Reset();
m_colorFilterIntensityIndex.Reset();
m_colorFilterMultiplyIndex.Reset();
m_colorFilterSwatchIndex.Reset();
m_whiteBalanceWeightIndex.Reset();
m_whiteBalanceKelvinIndex.Reset();
m_whiteBalanceTintIndex.Reset();
m_splitToneBalanceIndex.Reset();
m_splitToneWeightIndex.Reset();
m_colorGradingPostSaturationIndex.Reset();
m_splitToneShadowsColorIndex.Reset();
m_splitToneHighlightsColorIndex.Reset();
m_smhShadowsStartIndex.Reset();
m_smhShadowsEndIndex.Reset();
m_smhHighlightsStartIndex.Reset();
m_smhHighlightsEndIndex.Reset();
m_smhWeightIndex.Reset();
m_smhShadowsColorIndex.Reset();
m_smhMidtonesColorIndex.Reset();
m_smhHighlightsColorIndex.Reset();
m_channelMixingRedIndex.Reset();
m_channelMixingGreenIndex.Reset();
m_channelMixingBlueIndex.Reset();
m_colorFilterSwatchIndex.Reset();
m_splitToneShadowsColorIndex.Reset();
m_splitToneHighlightsColorIndex.Reset();
m_smhShadowsColorIndex.Reset();
m_smhMidtonesColorIndex.Reset();
m_smhHighlightsColorIndex.Reset();
m_finalAdjustmentWeightIndex.Reset();
m_colorGradingPostSaturationIndex.Reset();
m_colorGradingHueShiftIndex.Reset();
}
void HDRColorGradingPass::FrameBeginInternal(FramePrepareParams params)
@@ -79,33 +85,39 @@
const HDRColorGradingSettings* settings = GetHDRColorGradingSettings();
if (settings)
{
m_shaderResourceGroup->SetConstant(m_colorAdjustmentWeightIndex, settings->GetColorAdjustmentWeight());
m_shaderResourceGroup->SetConstant(m_colorGradingExposureIndex, settings->GetColorGradingExposure());
m_shaderResourceGroup->SetConstant(m_colorGradingContrastIndex, settings->GetColorGradingContrast());
m_shaderResourceGroup->SetConstant(m_colorGradingHueShiftIndex, settings->GetColorGradingHueShift());
m_shaderResourceGroup->SetConstant(m_colorGradingPreSaturationIndex, settings->GetColorGradingPreSaturation());
m_shaderResourceGroup->SetConstant(m_colorGradingPreSaturationIndex, settings->GetColorGradingPreSaturation() * 0.01f + 1.0f);
m_shaderResourceGroup->SetConstant(m_colorFilterIntensityIndex, settings->GetColorGradingFilterIntensity());
m_shaderResourceGroup->SetConstant(m_colorFilterMultiplyIndex, settings->GetColorGradingFilterMultiply());
m_shaderResourceGroup->SetConstant(m_colorFilterSwatchIndex, AZ::Vector4(settings->GetColorFilterSwatch()));
m_shaderResourceGroup->SetConstant(m_whiteBalanceWeightIndex, settings->GetWhiteBalanceWeight());
m_shaderResourceGroup->SetConstant(m_whiteBalanceKelvinIndex, settings->GetWhiteBalanceKelvin());
m_shaderResourceGroup->SetConstant(m_whiteBalanceTintIndex, settings->GetWhiteBalanceTint());
m_shaderResourceGroup->SetConstant(m_splitToneBalanceIndex, settings->GetSplitToneBalance());
m_shaderResourceGroup->SetConstant(m_splitToneWeightIndex, settings->GetSplitToneWeight());
m_shaderResourceGroup->SetConstant(m_colorGradingPostSaturationIndex, settings->GetColorGradingPostSaturation());
m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor()));
m_shaderResourceGroup->SetConstant(m_splitToneHighlightsColorIndex, AZ::Vector4(settings->GetSplitToneHighlightsColor()));
m_shaderResourceGroup->SetConstant(m_smhShadowsStartIndex, settings->GetSmhShadowsStart());
m_shaderResourceGroup->SetConstant(m_smhShadowsEndIndex, settings->GetSmhShadowsEnd());
m_shaderResourceGroup->SetConstant(m_smhHighlightsStartIndex, settings->GetSmhHighlightsStart());
m_shaderResourceGroup->SetConstant(m_smhHighlightsEndIndex, settings->GetSmhHighlightsEnd());
m_shaderResourceGroup->SetConstant(m_smhWeightIndex, settings->GetSmhWeight());
m_shaderResourceGroup->SetConstant(m_smhShadowsColorIndex, AZ::Vector4(settings->GetSmhShadowsColor()));
m_shaderResourceGroup->SetConstant(m_smhMidtonesColorIndex, AZ::Vector4(settings->GetSmhMidtonesColor()));
m_shaderResourceGroup->SetConstant(m_smhHighlightsColorIndex, AZ::Vector4(settings->GetSmhHighlightsColor()));
m_shaderResourceGroup->SetConstant(m_channelMixingRedIndex, settings->GetChannelMixingRed());
m_shaderResourceGroup->SetConstant(m_channelMixingGreenIndex, settings->GetChannelMixingGreen());
m_shaderResourceGroup->SetConstant(m_channelMixingBlueIndex, settings->GetChannelMixingBlue());
m_shaderResourceGroup->SetConstant(m_colorFilterSwatchIndex, AZ::Vector4(settings->GetColorFilterSwatch()));
m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor()));
m_shaderResourceGroup->SetConstant(m_splitToneHighlightsColorIndex, AZ::Vector4(settings->GetSplitToneHighlightsColor()));
m_shaderResourceGroup->SetConstant(m_smhShadowsColorIndex, AZ::Vector4(settings->GetSmhShadowsColor()));
m_shaderResourceGroup->SetConstant(m_smhMidtonesColorIndex, AZ::Vector4(settings->GetSmhMidtonesColor()));
m_shaderResourceGroup->SetConstant(m_smhHighlightsColorIndex, AZ::Vector4(settings->GetSmhHighlightsColor()));
m_shaderResourceGroup->SetConstant(m_finalAdjustmentWeightIndex, settings->GetFinalAdjustmentWeight());
m_shaderResourceGroup->SetConstant(m_colorGradingPostSaturationIndex, settings->GetColorGradingPostSaturation() * 0.01f + 1.0f);
m_shaderResourceGroup->SetConstant(m_colorGradingHueShiftIndex, settings->GetColorGradingHueShift());
}
}

Some files were not shown because too many files have changed in this diff Show More