From c29c1825cb519551356da38ac7cbfabfa500b4ab Mon Sep 17 00:00:00 2001 From: Doug McDiarmid Date: Mon, 24 May 2021 01:19:37 -0700 Subject: [PATCH 01/53] Added RayTracingPass and RayTracingPassData --- .../Code/Source/CommonSystemComponent.cpp | 6 + .../Code/Source/RayTracing/RayTracingPass.cpp | 326 ++++++++++++++++++ .../Code/Source/RayTracing/RayTracingPass.h | 81 +++++ .../Source/RayTracing/RayTracingPassData.h | 73 ++++ .../Code/atom_feature_common_files.cmake | 3 + .../RPI/Code/Include/Atom/RPI.Reflect/Base.h | 3 + 6 files changed, 492 insertions(+) create mode 100644 Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp create mode 100644 Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h create mode 100644 Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPassData.h diff --git a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp index 089a6168b1..d4053d0b31 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CommonSystemComponent.cpp @@ -91,6 +91,8 @@ #include #include +#include +#include #include #include #include @@ -132,6 +134,7 @@ namespace AZ SMAAFeatureProcessor::Reflect(context); PostProcessFeatureProcessor::Reflect(context); ImGuiPassData::Reflect(context); + RayTracingPassData::Reflect(context); LightingPreset::Reflect(context); ModelPreset::Reflect(context); @@ -275,6 +278,9 @@ namespace AZ passSystem->AddPassCreator(Name("ReflectionScreenSpaceBlurChildPass"), &Render::ReflectionScreenSpaceBlurChildPass::Create); passSystem->AddPassCreator(Name("ReflectionCopyFrameBufferPass"), &Render::ReflectionCopyFrameBufferPass::Create); + // Add RayTracing pas + passSystem->AddPassCreator(Name("RayTracingPass"), &Render::RayTracingPass::Create); + // setup handler for load pass template mappings m_loadTemplatesHandler = RPI::PassSystemInterface::OnReadyLoadTemplatesEvent::Handler([this]() { this->LoadPassTemplateMappings(); }); RPI::PassSystemInterface::Get()->ConnectEvent(m_loadTemplatesHandler); diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp new file mode 100644 index 0000000000..2c0c7e986e --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp @@ -0,0 +1,326 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + namespace Render + { + RPI::Ptr RayTracingPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew RayTracingPass(descriptor); + return pass; + } + + RayTracingPass::RayTracingPass(const RPI::PassDescriptor& descriptor) + : RenderPass(descriptor) + , m_passDescriptor(descriptor) + { + RHI::Ptr device = RHI::RHISystemInterface::Get()->GetDevice(); + if (device->GetFeatures().m_rayTracing == false) + { + // raytracing is not supported on this platform + SetEnabled(false); + } + + Init(); + } + + RayTracingPass::~RayTracingPass() + { + RPI::ShaderReloadNotificationBus::MultiHandler::BusDisconnect(); + } + + void RayTracingPass::Init() + { + RHI::Ptr device = RHI::RHISystemInterface::Get()->GetDevice(); + + m_passData = RPI::PassUtils::GetPassData(m_passDescriptor); + if (m_passData == nullptr) + { + AZ_Error("PassSystem", false, "RayTracingPass [%s]: Invalid RayTracingPassData", GetPathName().GetCStr()); + return; + } + + // ray generation shader + m_rayGenerationShader = LoadShader(m_passData->m_rayGenerationShaderAssetReference); + if (m_rayGenerationShader == nullptr) + { + AZ_Error("PassSystem", false, "RayTracingPass [%s]: Failed to load RayGeneration shader [%s]", GetPathName().GetCStr(), m_passData->m_rayGenerationShaderAssetReference.m_filePath.data()); + return; + } + + auto shaderVariant = m_rayGenerationShader->GetVariant(RPI::ShaderAsset::RootShaderVariantStableId); + RHI::PipelineStateDescriptorForRayTracing rayGenerationShaderDescriptor; + shaderVariant.ConfigurePipelineState(rayGenerationShaderDescriptor); + + // closest hit shader + m_closestHitShader = LoadShader(m_passData->m_closestHitShaderAssetReference); + if (m_closestHitShader == nullptr) + { + AZ_Error("PassSystem", false, "RayTracingPass [%s]: Failed to load ClosestHit shader [%s]", GetPathName().GetCStr(), m_passData->m_closestHitShaderAssetReference.m_filePath.data()); + return; + } + + shaderVariant = m_closestHitShader->GetVariant(RPI::ShaderAsset::RootShaderVariantStableId); + RHI::PipelineStateDescriptorForRayTracing closestHitShaderDescriptor; + shaderVariant.ConfigurePipelineState(closestHitShaderDescriptor); + + // miss shader + m_missShader = LoadShader(m_passData->m_missShaderAssetReference); + if (m_missShader == nullptr) + { + AZ_Error("PassSystem", false, "RayTracingPass [%s]: Failed to load Miss shader [%s]", GetPathName().GetCStr(), m_passData->m_missShaderAssetReference.m_filePath.data()); + return; + } + + shaderVariant = m_missShader->GetVariant(RPI::ShaderAsset::RootShaderVariantStableId); + RHI::PipelineStateDescriptorForRayTracing missShaderDescriptor; + shaderVariant.ConfigurePipelineState(missShaderDescriptor); + + // retrieve global pipeline state + m_globalPipelineState = m_rayGenerationShader->AcquirePipelineState(rayGenerationShaderDescriptor); + AZ_Assert(m_globalPipelineState, "Failed to acquire ray tracing global pipeline state"); + + // create global srg + static const uint32_t RayTracingGlobalSrgBindingSlot = 0; + Data::Asset globalSrgAsset = m_rayGenerationShader->FindShaderResourceGroupAsset(RayTracingGlobalSrgBindingSlot); + AZ_Error("PassSystem", globalSrgAsset.GetId().IsValid(), "RayTracingPass [%s] Failed to find RayTracingGlobalSrg asset", GetPathName().GetCStr()); + AZ_Error("PassSystem", globalSrgAsset.IsReady(), "RayTracingPass [%s] asset is not loaded for shader", GetPathName().GetCStr()); + + m_shaderResourceGroup = RPI::ShaderResourceGroup::Create(globalSrgAsset); + AZ_Assert(m_shaderResourceGroup, "RayTracingPass [%s]: Failed to create RayTracingGlobalSrg", GetPathName().GetCStr()); + RPI::PassUtils::BindDataMappingsToSrg(m_passDescriptor, m_shaderResourceGroup.get()); + + // check to see if the shader requires a ViewSrg + Data::Asset viewSrgAsset = m_rayGenerationShader->FindShaderResourceGroupAsset(RPI::SrgBindingSlot::View); + m_requiresViewSrg = viewSrgAsset.GetId().IsValid(); + + // build the ray tracing pipeline state descriptor + RHI::RayTracingPipelineStateDescriptor descriptor; + descriptor.Build() + ->PipelineState(m_globalPipelineState.get()) + ->MaxPayloadSize(m_passData->m_maxPayloadSize) + ->MaxAttributeSize(m_passData->m_maxAttributeSize) + ->MaxRecursionDepth(m_passData->m_maxRecursionDepth) + ->ShaderLibrary(rayGenerationShaderDescriptor) + ->RayGenerationShaderName(AZ::Name(m_passData->m_rayGenerationShaderName.c_str())) + ->ShaderLibrary(missShaderDescriptor) + ->MissShaderName(AZ::Name(m_passData->m_missShaderName.c_str())) + ->ShaderLibrary(closestHitShaderDescriptor) + ->ClosestHitShaderName(AZ::Name(m_passData->m_closestHitShaderName.c_str())) + ->HitGroup(AZ::Name("HitGroup")) + ->ClosestHitShaderName(AZ::Name(m_passData->m_closestHitShaderName.c_str())); + + // create the ray tracing pipeline state object + m_rayTracingPipelineState = RHI::Factory::Get().CreateRayTracingPipelineState(); + m_rayTracingPipelineState->Init(*device.get(), &descriptor); + + // make sure the shader table rebuilds if we're hotreloading + m_rayTracingRevision = 0; + + RPI::ShaderReloadNotificationBus::MultiHandler::BusDisconnect(); + RPI::ShaderReloadNotificationBus::MultiHandler::BusConnect(m_passData->m_rayGenerationShaderAssetReference.m_assetId); + RPI::ShaderReloadNotificationBus::MultiHandler::BusConnect(m_passData->m_closestHitShaderAssetReference.m_assetId); + RPI::ShaderReloadNotificationBus::MultiHandler::BusConnect(m_passData->m_missShaderAssetReference.m_assetId); + } + + Data::Instance RayTracingPass::LoadShader(const RPI::AssetReference& shaderAssetReference) + { + Data::Asset shaderAsset; + if (shaderAssetReference.m_assetId.IsValid()) + { + shaderAsset = RPI::FindShaderAsset(shaderAssetReference.m_assetId, shaderAssetReference.m_filePath); + } + + if (!shaderAsset.GetId().IsValid()) + { + AZ_Error("PassSystem", false, "RayTracingPass [%s]: Failed to load shader asset [%s]", GetPathName().GetCStr(), shaderAssetReference.m_filePath.data()); + return nullptr; + } + + return RPI::Shader::FindOrCreate(shaderAsset); + } + + void RayTracingPass::FrameBeginInternal(FramePrepareParams params) + { + RPI::Scene* scene = m_pipeline->GetScene(); + RayTracingFeatureProcessor* rayTracingFeatureProcessor = scene->GetFeatureProcessor(); + if (!rayTracingFeatureProcessor) + { + return; + } + + if (!m_rayTracingShaderTable) + { + RHI::Ptr device = RHI::RHISystemInterface::Get()->GetDevice(); + RHI::RayTracingBufferPools& rayTracingBufferPools = rayTracingFeatureProcessor->GetBufferPools(); + + m_rayTracingShaderTable = RHI::Factory::Get().CreateRayTracingShaderTable(); + m_rayTracingShaderTable->Init(*device.get(), rayTracingBufferPools); + } + + RPI::RenderPass::FrameBeginInternal(params); + } + + void RayTracingPass::SetupFrameGraphDependencies(RHI::FrameGraphInterface frameGraph) + { + RPI::RenderPass::SetupFrameGraphDependencies(frameGraph); + frameGraph.SetEstimatedItemCount(1); + } + + void RayTracingPass::CompileResources(const RHI::FrameGraphCompileContext& context) + { + RPI::Scene* scene = m_pipeline->GetScene(); + RayTracingFeatureProcessor* rayTracingFeatureProcessor = scene->GetFeatureProcessor(); + AZ_Assert(rayTracingFeatureProcessor, "RayTracingPass requires the RayTracingFeatureProcessor"); + + if (m_shaderResourceGroup != nullptr) + { + BindPassSrg(context, m_shaderResourceGroup); + m_shaderResourceGroup->Compile(); + } + + uint32_t rayTracingRevision = rayTracingFeatureProcessor->GetRevision(); + if (m_rayTracingRevision != rayTracingRevision) + { + // scene changed, need to rebuild the shader table + m_rayTracingRevision = rayTracingRevision; + + AZStd::shared_ptr descriptor = AZStd::make_shared(); + + if (rayTracingFeatureProcessor->GetSubMeshCount()) + { + // build the ray tracing shader table descriptor + RHI::RayTracingShaderTableDescriptor* descriptorBuild = descriptor->Build(AZ::Name("RayTracingShaderTable"), m_rayTracingPipelineState) + ->RayGenerationRecord(AZ::Name(m_passData->m_rayGenerationShaderName.c_str())) + ->MissRecord(AZ::Name(m_passData->m_missShaderName.c_str())); + + // add a hit group for each mesh to the shader table + for (uint32_t i = 0; i < rayTracingFeatureProcessor->GetSubMeshCount(); ++i) + { + descriptorBuild->HitGroupRecord(AZ::Name("HitGroup")); + } + } + + m_rayTracingShaderTable->Build(descriptor); + } + } + + void RayTracingPass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) + { + RPI::Scene* scene = m_pipeline->GetScene(); + RayTracingFeatureProcessor* rayTracingFeatureProcessor = scene->GetFeatureProcessor(); + AZ_Assert(rayTracingFeatureProcessor, "RayTracingPass requires the RayTracingFeatureProcessor"); + + if (!rayTracingFeatureProcessor || + !rayTracingFeatureProcessor->GetTlas()->GetTlasBuffer() || + !rayTracingFeatureProcessor->GetSubMeshCount() || + !m_rayTracingShaderTable) + { + return; + } + + RHI::DispatchRaysItem dispatchRaysItem; + + // calculate thread counts if this is a full screen raytracing pass + if (m_passData->m_makeFullscreenPass) + { + RPI::PassAttachment* outputAttachment = nullptr; + + if (GetOutputCount() > 0) + { + outputAttachment = GetOutputBinding(0).m_attachment.get(); + } + else if (GetInputOutputCount() > 0) + { + outputAttachment = GetInputOutputBinding(0).m_attachment.get(); + } + + AZ_Assert(outputAttachment != nullptr, "[RayTracingPass '%s']: A fullscreen RayTracing pass must have a valid output or input/output.", GetPathName().GetCStr()); + AZ_Assert(outputAttachment->GetAttachmentType() == RHI::AttachmentType::Image, "[RayTracingPass '%s']: The output of a fullscreen RayTracing pass must be an image.", GetPathName().GetCStr()); + + RHI::Size imageSize = outputAttachment->m_descriptor.m_image.m_size; + + dispatchRaysItem.m_width = imageSize.m_width; + dispatchRaysItem.m_height = imageSize.m_height; + dispatchRaysItem.m_depth = imageSize.m_depth; + } + else + { + dispatchRaysItem.m_width = m_passData->m_threadCountX; + dispatchRaysItem.m_height = m_passData->m_threadCountY; + dispatchRaysItem.m_depth = m_passData->m_threadCountZ; + } + + // bind RayTracingGlobal, RayTracingScene, and View Srgs + // [GFX TODO][ATOM-15610] Add RenderPass::SetSrgsForRayTracingDispatch + AZStd::vector shaderResourceGroups = + { + m_shaderResourceGroup->GetRHIShaderResourceGroup(), + rayTracingFeatureProcessor->GetRayTracingSceneSrg()->GetRHIShaderResourceGroup() + }; + + if (m_requiresViewSrg) + { + const AZStd::vector& views = m_pipeline->GetViews(m_passData->m_pipelineViewTag); + if (views.size() > 0) + { + shaderResourceGroups.push_back(views[0]->GetRHIShaderResourceGroup()); + } + } + + dispatchRaysItem.m_shaderResourceGroupCount = aznumeric_cast(shaderResourceGroups.size()); + dispatchRaysItem.m_shaderResourceGroups = shaderResourceGroups.data(); + dispatchRaysItem.m_rayTracingPipelineState = m_rayTracingPipelineState.get(); + dispatchRaysItem.m_rayTracingShaderTable = m_rayTracingShaderTable.get(); + dispatchRaysItem.m_globalPipelineState = m_globalPipelineState.get(); + + // submit the DispatchRays item + context.GetCommandList()->Submit(dispatchRaysItem); + } + + void RayTracingPass::OnShaderReinitialized([[maybe_unused]] const RPI::Shader& shader) + { + Init(); + } + + void RayTracingPass::OnShaderAssetReinitialized([[maybe_unused]] const Data::Asset& shaderAsset) + { + Init(); + } + + void RayTracingPass::OnShaderVariantReinitialized([[maybe_unused]] const RPI::Shader& shader, [[maybe_unused]] const RPI::ShaderVariantId& shaderVariantId, [[maybe_unused]] RPI::ShaderVariantStableId shaderVariantStableId) + { + Init(); + } + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h new file mode 100644 index 0000000000..935d034513 --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h @@ -0,0 +1,81 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + namespace Render + { + struct RayTracingPassData; + + //! This pass executes a raytracing shader as specified in the PassData. + class RayTracingPass + : public RPI::RenderPass + , private RPI::ShaderReloadNotificationBus::MultiHandler + { + AZ_RPI_PASS(RayTracingPass); + + public: + AZ_RTTI(RayTracingPass, "{7A68A36E-956A-4258-93FE-38686042C4D9}", RPI::RenderPass); + AZ_CLASS_ALLOCATOR(RayTracingPass, SystemAllocator, 0); + virtual ~RayTracingPass(); + + //! Creates a RayTracingPass + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + RayTracingPass(const RPI::PassDescriptor& descriptor); + + // Pass overrides + void FrameBeginInternal(FramePrepareParams params) override; + + // Scope producer functions + void SetupFrameGraphDependencies(RHI::FrameGraphInterface frameGraph) override; + void CompileResources(const RHI::FrameGraphCompileContext& context) override; + void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override; + + // ShaderReloadNotificationBus::Handler overrides + void OnShaderReinitialized(const RPI::Shader& shader) override; + void OnShaderAssetReinitialized(const Data::Asset& shaderAsset) override; + void OnShaderVariantReinitialized(const RPI::Shader& shader, const RPI::ShaderVariantId& shaderVariantId, RPI::ShaderVariantStableId shaderVariantStableId) override; + + // load the raytracing shaders and setup pipeline states + void Init(); + + // helper for loading a shader from a shader asset reference + Data::Instance LoadShader(const RPI::AssetReference& shaderAssetReference); + + // pass data + RPI::PassDescriptor m_passDescriptor; + const RayTracingPassData* m_passData = nullptr; + + // revision number of the ray tracing TLAS when the shader table was built + uint32_t m_rayTracingRevision = 0; + + // raytracing shaders, pipeline states, and shader table + Data::Instance m_rayGenerationShader; + Data::Instance m_missShader; + Data::Instance m_closestHitShader; + RHI::Ptr m_rayTracingPipelineState; + RHI::ConstPtr m_globalPipelineState; + RHI::Ptr m_rayTracingShaderTable; + bool m_requiresViewSrg = false; + }; + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPassData.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPassData.h new file mode 100644 index 0000000000..bc15a8372c --- /dev/null +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPassData.h @@ -0,0 +1,73 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once + +#include +#include + +namespace AZ +{ + namespace Render + { + //! Custom data for the RayTracingPass, specified in the PassRequest. + struct RayTracingPassData + : public RPI::RenderPassData + { + AZ_RTTI(RayTracingPassData, "{26C2E2FD-D30A-4142-82A3-0167BC94B3EE}", RPI::RenderPassData); + AZ_CLASS_ALLOCATOR(RayTracingPassData, SystemAllocator, 0); + + RayTracingPassData() = default; + virtual ~RayTracingPassData() = default; + + static void Reflect(ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("RayGenerationShaderAsset", &RayTracingPassData::m_rayGenerationShaderAssetReference) + ->Field("RayGenerationShaderName", &RayTracingPassData::m_rayGenerationShaderName) + ->Field("ClosestHitShaderAsset", &RayTracingPassData::m_closestHitShaderAssetReference) + ->Field("ClosestHitShaderName", &RayTracingPassData::m_closestHitShaderName) + ->Field("MissShaderAsset", &RayTracingPassData::m_missShaderAssetReference) + ->Field("MissShaderName", &RayTracingPassData::m_missShaderName) + ->Field("MaxPayloadSize", &RayTracingPassData::m_maxPayloadSize) + ->Field("MaxAttributeSize", &RayTracingPassData::m_maxAttributeSize) + ->Field("MaxRecursionDepth", &RayTracingPassData::m_maxRecursionDepth) + ->Field("Thread Count X", &RayTracingPassData::m_threadCountX) + ->Field("Thread Count Y", &RayTracingPassData::m_threadCountY) + ->Field("Thread Count Z", &RayTracingPassData::m_threadCountZ) + ->Field("Make Fullscreen Pass", &RayTracingPassData::m_makeFullscreenPass) + ; + } + } + + RPI::AssetReference m_rayGenerationShaderAssetReference; + AZStd::string m_rayGenerationShaderName; + RPI::AssetReference m_closestHitShaderAssetReference; + AZStd::string m_closestHitShaderName; + RPI::AssetReference m_missShaderAssetReference; + AZStd::string m_missShaderName; + + uint32_t m_maxPayloadSize = 64; + uint32_t m_maxAttributeSize = 32; + uint32_t m_maxRecursionDepth = 1; + + uint32_t m_threadCountX = 1; + uint32_t m_threadCountY = 1; + uint32_t m_threadCountZ = 1; + + bool m_makeFullscreenPass = false; + }; + } // namespace RPI +} // namespace AZ + diff --git a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake index 8926b0c19f..064e49922d 100644 --- a/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake +++ b/Gems/Atom/Feature/Common/Code/atom_feature_common_files.cmake @@ -252,6 +252,9 @@ set(FILES Source/RayTracing/RayTracingFeatureProcessor.cpp Source/RayTracing/RayTracingAccelerationStructurePass.cpp Source/RayTracing/RayTracingAccelerationStructurePass.h + Source/RayTracing/RayTracingPass.cpp + Source/RayTracing/RayTracingPass.h + Source/RayTracing/RayTracingPassData.h Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp Source/ReflectionProbe/ReflectionProbe.cpp Source/ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.cpp diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Base.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Base.h index c376112290..52b75c4157 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Base.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Base.h @@ -59,7 +59,10 @@ namespace AZ static constexpr uint32_t Draw = 0; static constexpr uint32_t Object = 1; static constexpr uint32_t Material = 2; + static constexpr uint32_t SubPass = 3; static constexpr uint32_t Pass = 4; + static constexpr uint32_t View = 5; + static constexpr uint32_t Scene = 6; }; } } From 12d0d9e7b78edff156a4cdc3abfb42879126eab5 Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Mon, 24 May 2021 12:49:38 -0700 Subject: [PATCH 02/53] The host will now have autonomy over the default player is has spawned for itself using the sv_defaultPlayerSpawnAsset cvar --- Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index ef8627fe54..39bbf6644e 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -449,6 +449,7 @@ namespace Multiplayer { controlledEntity = entityList[0]; controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); + controlledEntity.GetNetBindComponent()->SetAllowAutonomy(true); } if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so From b5a0df00e1a14ad5f9aefb1063612953b3db8a5d Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Tue, 25 May 2021 17:21:03 -0700 Subject: [PATCH 03/53] Hosts which are not a dedicated server (meaning they also play the game) will spawn a default player for themselves --- .../Source/MultiplayerSystemComponent.cpp | 36 ++++++++++++++----- .../Code/Source/MultiplayerSystemComponent.h | 3 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 39bbf6644e..0818f605df 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -438,20 +438,16 @@ namespace Multiplayer m_connAcquiredEvent.Signal(datum); } + // Hosts will spawn a new default player prefab for the user that just connected if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str()), 1); - INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity()); - - NetworkEntityHandle controlledEntity; - if (entityList.size() > 0) + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); + if (controlledEntity.Exists()) { - controlledEntity = entityList[0]; controlledEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); - controlledEntity.GetNetBindComponent()->SetAllowAutonomy(true); } - + if (connection->GetUserData() == nullptr) // Only add user data if the connect event handler has not already done so { connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); @@ -523,6 +519,17 @@ namespace Multiplayer } } m_agentType = multiplayerType; + + // Spawn the default player for this host since the host is also a player (not a dedicated server) + if (m_agentType == MultiplayerAgentType::ClientServer) + { + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(); + if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) + { + controlledEntityNetBindComponent->SetAllowAutonomy(true); + } + } + AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); } @@ -630,6 +637,19 @@ namespace Multiplayer } } + NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + { + PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str()), 1); + INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity()); + + NetworkEntityHandle controlledEntity; + if (entityList.size() > 0) + { + controlledEntity = entityList[0]; + } + return controlledEntity; + } + void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { Multiplayer::MultiplayerAgentType serverType = sv_isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer; diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index db83c50fb5..e8e05a9d4c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -104,7 +104,8 @@ namespace Multiplayer void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - + NetworkEntityHandle SpawnDefaultPlayerPrefab(); + AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; From 0fc9697e495d13ef8dd45bbc5723b79539336b06 Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Tue, 25 May 2021 19:56:18 -0700 Subject: [PATCH 04/53] Allow Autonomous->Auth properties. Remove the ability of accessing properties Getters from the Component when ReplicateTo is Autonomous; in this case users must be using the controller to Get --- .../Source/AutoGen/AutoComponent_Header.jinja | 5 +++-- .../Source/AutoGen/AutoComponent_Source.jinja | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja index 71e81b6bfb..aeab2e88e1 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja @@ -365,6 +365,8 @@ namespace {{ Component.attrib['Namespace'] }} {{ DeclareNetworkPropertyAccessors(Component, 'Authority', 'Client', true)|indent(8) -}} {{ DeclareNetworkPropertyAccessors(Component, 'Authority', 'Autonomous', false)|indent(8) -}} {{ DeclareNetworkPropertyAccessors(Component, 'Authority', 'Autonomous', true)|indent(8) -}} + {{ DeclareNetworkPropertyAccessors(Component, 'Autonomous', 'Authority', false)|indent(8) -}} + {{ DeclareNetworkPropertyAccessors(Component, 'Autonomous', 'Authority', true)|indent(8) -}} {{ DeclareArchetypePropertyGetters(Component)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', false)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', true)|indent(8) -}} @@ -438,7 +440,7 @@ namespace {{ Component.attrib['Namespace'] }} {% endif %} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Server', false)|indent(8) -}} - {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Autonomous', false)|indent(8) -}} + {{ DeclareNetworkPropertyGetters(Component, 'Autonomous', 'Authority', false)|indent(8) -}} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Client', false)|indent(8) -}} {{ DeclareArchetypePropertyGetters(Component)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', false)|indent(8) -}} @@ -463,7 +465,6 @@ namespace {{ Component.attrib['Namespace'] }} //! @} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Server', true)|indent(8) -}} - {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Autonomous', true)|indent(8) -}} {{ DeclareNetworkPropertyGetters(Component, 'Autonomous', 'Authority', true)|indent(8) -}} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Client', true)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', true)|indent(8) -}} diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja index 7d5295aabb..3ca4d854bb 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja @@ -767,18 +767,33 @@ enum class NetworkProperties return {{ Property.attrib['Type'] }}(); } - {{ ClassName }}* networkComponent = entity->FindComponent<{{ ClassName }}>(); + {{ ClassName }}* networkComponent = entity->FindComponent<{{ ClassName }}>(); if (!networkComponent) { AZ_Warning("Network Property", false, "{{ ClassName }} Get{{ UpperFirst(Property.attrib['Name']) }} failed. Entity '%s' (id: %s) is missing {{ ClassName }}, be sure to add {{ ClassName }} to this entity.", entity->GetName().c_str(), id.ToString().c_str()) return {{ Property.attrib['Type'] }}(); } +{% if ReplicateTo == 'Autonomous' %} + // {{ UpperFirst(Property.attrib['Name']) }} is replicated to Automonous; we must go through the controller in order to get this property + {{ ClassName }}Controller* controller = static_cast<{{ ClassName }}Controller*>(networkComponent->GetController()); + if (!controller) + { + AZ_Warning("Network Property", false, "{{ ClassName }} Get{{ UpperFirst(Property.attrib['Name']) }} method failed. Entity '%s' (id: %s) {{ ClassName }} is missing the network controller. This property is replicated to autonomous network entities, because this entity doesn't have a controller, it must not be automonous. Please check your network context before attempting to get {{ UpperFirst(Property.attrib['Name']) }}.", entity->GetName().c_str(), id.ToString().c_str()) + return {{ Property.attrib['Type'] }}(); + } +{% if Property.attrib['Container'] == 'Vector' or Property.attrib['Container'] == 'Array' %} + return controller->Get{{ UpperFirst(Property.attrib['Name']) }}(index); +{% else %} + return controller->Get{{ UpperFirst(Property.attrib['Name']) }}(); +{% endif %} +{% else %} {% if Property.attrib['Container'] == 'Vector' or Property.attrib['Container'] == 'Array' %} return networkComponent->Get{{ UpperFirst(Property.attrib['Name']) }}(index); {% else %} return networkComponent->Get{{ UpperFirst(Property.attrib['Name']) }}(); {% endif %} +{% endif %} }) {% if Property.attrib['Container'] == 'Vector' or Property.attrib['Container'] == 'Array' %} ->Method("Set{{ UpperFirst(Property.attrib['Name']) }}", [](AZ::EntityId id, int32_t index, const {{ Property.attrib['Type'] }}& value) -> void @@ -1457,11 +1472,9 @@ namespace {{ Component.attrib['Namespace'] }} } {{ DefineNetworkPropertyGets(Component, 'Authority', 'Server', false, ComponentBaseName)|indent(4) -}} -{{ DefineNetworkPropertyGets(Component, 'Authority', 'Autonomous', false, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Autonomous', 'Authority', false, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Client', false, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Server', true, ComponentBaseName)|indent(4) -}} -{{ DefineNetworkPropertyGets(Component, 'Authority', 'Autonomous', true, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Autonomous', 'Authority', true, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Client', true, ComponentBaseName)|indent(4) }} {{ DefineArchetypePropertyGets(Component, ClassType, ComponentBaseName)|indent(4) -}} From 40435fcb2e5326bc28cb750aaa3df270ba263ff9 Mon Sep 17 00:00:00 2001 From: Doug McDiarmid Date: Tue, 25 May 2021 23:52:19 -0700 Subject: [PATCH 05/53] Added RayTracingMaterialSrg. Added UV buffer to the RayTracingSceneSrg mesh buffers. Added RayTracingSceneUtils and RayTracingMaterialUtils shader includes. --- .../RayTracing/RayTracingMaterialSrg.azsli | 48 ++++ .../RayTracing/RayTracingMaterialUtils.azsli | 69 +++++ .../RayTracing/RayTracingSceneSrg.azsli | 33 ++- .../RayTracing/RayTracingSceneUtils.azsli | 126 +++++++++ .../Atom/Features/SrgSemantics.azsli | 5 + .../diffuseprobegridraytracing.azshader | Bin 79334 -> 79466 bytes ...probegridraytracing_dx12_0.azshadervariant | Bin 34086 -> 34738 bytes ...probegridraytracing_null_0.azshadervariant | Bin 4502 -> 4502 bytes ...obegridraytracing_vulkan_0.azshadervariant | Bin 36232 -> 36332 bytes ...fuseprobegridraytracingclosesthit.azshader | Bin 79344 -> 79476 bytes ...aytracingclosesthit_dx12_0.azshadervariant | Bin 16754 -> 17150 bytes ...aytracingclosesthit_null_0.azshadervariant | Bin 4502 -> 4502 bytes ...tracingclosesthit_vulkan_0.azshadervariant | Bin 9172 -> 9452 bytes ...raytracingcommon_raytracingglobalsrg.azsrg | 24 +- .../diffuseprobegridraytracingmiss.azshader | Bin 79338 -> 79470 bytes ...egridraytracingmiss_dx12_0.azshadervariant | Bin 16838 -> 17222 bytes ...egridraytracingmiss_null_0.azshadervariant | Bin 4502 -> 4502 bytes ...ridraytracingmiss_vulkan_0.azshadervariant | Bin 10364 -> 10364 bytes .../Code/Source/Mesh/MeshFeatureProcessor.cpp | 213 +++++++++++++--- .../RayTracingAccelerationStructurePass.cpp | 4 +- .../RayTracing/RayTracingFeatureProcessor.cpp | 240 ++++++++++++++---- .../RayTracing/RayTracingFeatureProcessor.h | 111 +++++++- .../Code/Source/RayTracing/RayTracingPass.cpp | 11 +- .../Code/Source/RayTracing/RayTracingPass.h | 1 + 24 files changed, 777 insertions(+), 108 deletions(-) create mode 100644 Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialSrg.azsli create mode 100644 Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialUtils.azsli create mode 100644 Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneUtils.azsli diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialSrg.azsli new file mode 100644 index 0000000000..40faba24f9 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialSrg.azsli @@ -0,0 +1,48 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include + +ShaderResourceGroup RayTracingMaterialSrg : SRG_RayTracingMaterial +{ + Sampler LinearSampler + { + AddressU = Wrap; + AddressV = Wrap; + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + MaxAnisotropy = 16; + }; + + // material info structured buffer + struct MaterialInfo + { + float4 m_baseColor; + float m_metallicFactor; + float m_roughnessFactor; + uint m_textureFlags; + uint m_textureStartIndex; + }; + + // hit shaders can retrieve the MaterialInfo for a mesh hit using: RayTracingMaterialSrg::m_materialInfo[InstanceIndex()] + StructuredBuffer m_materialInfo; + + // texture flag bits indicating if optional textures are present + #define TEXTURE_FLAG_BASECOLOR 1 + #define TEXTURE_FLAG_NORMAL 2 + #define TEXTURE_FLAG_METALLIC 4 + #define TEXTURE_FLAG_ROUGHNESS 8 + + // unbounded array of Material textures + Texture2D m_materialTextures[]; +} \ No newline at end of file diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialUtils.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialUtils.azsli new file mode 100644 index 0000000000..d6dd77f4fa --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingMaterialUtils.azsli @@ -0,0 +1,69 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +struct TextureData +{ + float4 m_baseColor; + float3 m_normal; + float m_metallic; + float m_roughness; +}; + +TextureData GetHitTextureData(RayTracingMaterialSrg::MaterialInfo materialInfo, float2 uv) +{ + TextureData textureData = (TextureData)0; + + uint textureIndex = materialInfo.m_textureStartIndex; + + // base color + if (materialInfo.m_textureFlags & TEXTURE_FLAG_BASECOLOR) + { + textureData.m_baseColor = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0); + } + else + { + textureData.m_baseColor = materialInfo.m_baseColor; + } + + // normal + if (materialInfo.m_textureFlags & TEXTURE_FLAG_NORMAL) + { + textureData.m_normal = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0); + } + else + { + textureData.m_normal = float3(0.0f, 0.0f, 1.0f); + } + + // metallic + if (materialInfo.m_textureFlags & TEXTURE_FLAG_METALLIC) + { + textureData.m_metallic = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0); + } + else + { + textureData.m_metallic = materialInfo.m_metallicFactor; + } + + // roughness + if (materialInfo.m_textureFlags & TEXTURE_FLAG_ROUGHNESS) + { + textureData.m_roughness = RayTracingMaterialSrg::m_materialTextures[textureIndex++].SampleLevel(RayTracingMaterialSrg::LinearSampler, uv, 0); + } + else + { + textureData.m_roughness = materialInfo.m_roughnessFactor; + } + + return textureData; +} + \ No newline at end of file diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli index fdf7ba92de..2352f5d09b 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli @@ -136,18 +136,35 @@ ShaderResourceGroup RayTracingSceneSrg : SRG_RayTracingScene uint m_indexOffset; uint m_positionOffset; uint m_normalOffset; + uint m_tangentOffset; + uint m_bitangentOffset; + uint m_uvOffset; + float m_padding0[2]; + float4 m_irradianceColor; float3x3 m_worldInvTranspose; + float m_padding1[1]; + + uint m_bufferFlags; + uint m_bufferStartIndex; }; - + + // hit shaders can retrieve the MeshInfo for a mesh hit using: RayTracingSceneSrg::m_meshInfo[InstanceIndex()] StructuredBuffer m_meshInfo; - // unbounded array of Index, VertexPosition, and VertexNormal buffers - // each mesh has three entries in this array starting at its InstanceIndex() * BUFFER_COUNT_PER_MESH - #define BUFFER_COUNT_PER_MESH 3 - #define MESH_INDEX_BUFFER_OFFSET 0 - #define MESH_POSITION_BUFFER_OFFSET 1 - #define MESH_NORMAL_BUFFER_OFFSET 2 - + // buffer array index offsets for buffers that are always present for each mesh + #define MESH_INDEX_BUFFER_OFFSET 0 + #define MESH_POSITION_BUFFER_OFFSET 1 + #define MESH_NORMAL_BUFFER_OFFSET 2 + #define MESH_TANGENT_BUFFER_OFFSET 3 + #define MESH_BITANGENT_BUFFER_OFFSET 4 + + // buffer flag bits indicating if optional buffers are present + #define MESH_BUFFER_FLAG_UV 1 + + // Unbounded array of mesh stream buffers: + // - Index, Position, Normal, Tangent, and Bitangent stream buffers are always present + // - Optional stream buffers such as UV are indicated in the MeshInfo.m_bufferFlags field + // - Buffers for a particular mesh start at MeshInfo.m_bufferStartIndex ByteAddressBuffer m_meshBuffers[]; } \ No newline at end of file diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneUtils.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneUtils.azsli new file mode 100644 index 0000000000..f858b9a11a --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneUtils.azsli @@ -0,0 +1,126 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +// returns the normalized camera view ray into the scene for this raytracing dispatch thread +float3 GetViewRayDirection(float4x4 viewProjectionInverseMatrix) +{ + float2 pixel = ((float2)DispatchRaysIndex().xy + float2(0.5f, 0.5f)) / (float2)DispatchRaysDimensions(); + float2 ndc = pixel * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f); + return normalize(mul(viewProjectionInverseMatrix, float4(ndc, 0.0f, 1.0f)).xyz); +} + +// returns the vertex indices for the primitive hit by the ray +// Note: usable only in a raytracing Hit shader +uint3 GetHitIndices(RayTracingSceneSrg::MeshInfo meshInfo) +{ + // compute the array index of the index buffer for this mesh in the m_meshBuffers unbounded array + uint meshIndexBufferArrayIndex = meshInfo.m_bufferStartIndex + MESH_INDEX_BUFFER_OFFSET; + + // compute the offset into the index buffer for this primitve of the mesh + uint offsetBytes = meshInfo.m_indexOffset + (PrimitiveIndex() * 12); + + // load the indices for this primitive from the index buffer + return RayTracingSceneSrg::m_meshBuffers[meshIndexBufferArrayIndex].Load3(offsetBytes); +} + +// returns the interpolated vertex data for the primitive hit by the ray +// Note: usable only in a raytracing hit shader +struct VertexData +{ + float3 m_position; + float3 m_normal; + float3 m_tangent; + float3 m_bitangent; + float2 m_uv; +}; + +VertexData GetHitInterpolatedVertexData(RayTracingSceneSrg::MeshInfo meshInfo, float2 builtInBarycentrics) +{ + // retrieve the poly indices + uint3 indices = GetHitIndices(meshInfo); + + // compute barycentrics + float3 barycentrics = float3((1.0f - builtInBarycentrics.x - builtInBarycentrics.y), builtInBarycentrics.x, builtInBarycentrics.y); + + // compute the vertex data using barycentric interpolation + VertexData vertexData = (VertexData)0; + for (uint i = 0; i < 3; ++i) + { + // position + { + // array index of the position buffer for this mesh in the m_meshBuffers unbounded array + uint meshVertexPositionArrayIndex = meshInfo.m_bufferStartIndex + MESH_POSITION_BUFFER_OFFSET; + + // offset into the position buffer for this vertex + uint positionOffset = meshInfo.m_positionOffset + (indices[i] * 12); + + // load the position data + vertexData.m_position += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexPositionArrayIndex].Load3(positionOffset)) * barycentrics[i]; + } + + // normal + { + // array index of the normal buffer for this mesh in the m_meshBuffers unbounded array + uint meshVertexNormalArrayIndex = meshInfo.m_bufferStartIndex + MESH_NORMAL_BUFFER_OFFSET; + + // offset into the normal buffer for this vertex + uint normalOffset = meshInfo.m_normalOffset + (indices[i] * 12); + + // load the normal data + vertexData.m_normal += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexNormalArrayIndex].Load3(normalOffset)) * barycentrics[i]; + } + + // tangent + { + // array index of the tangent buffer for this mesh in the m_meshBuffers unbounded array + uint meshVertexTangentArrayIndex = meshInfo.m_bufferStartIndex + MESH_TANGENT_BUFFER_OFFSET; + + // offset into the tangent buffer for this vertex + uint tangentOffset = meshInfo.m_tangentOffset + (indices[i] * 12); + + // load the tangent data + vertexData.m_tangent += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexTangentArrayIndex].Load3(tangentOffset)) * barycentrics[i]; + } + + // bitangent + { + // array index of the bitangent buffer for this mesh in the m_meshBuffers unbounded array + uint meshVertexBitangentArrayIndex = meshInfo.m_bufferStartIndex + MESH_BITANGENT_BUFFER_OFFSET; + + // offset into the bitangent buffer for this vertex + uint bitangentOffset = meshInfo.m_bitangentOffset + (indices[i] * 12); + + // load the bitangent data + vertexData.m_bitangent += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexBitangentArrayIndex].Load3(bitangentOffset)) * barycentrics[i]; + } + + // optional streams begin after MESH_BITANGENT_BUFFER_OFFSET + uint optionalBufferOffset = MESH_BITANGENT_BUFFER_OFFSET + 1; + + // UV + if (meshInfo.m_bufferFlags & MESH_BUFFER_FLAG_UV) + { + // array index of the UV buffer for this mesh in the m_meshBuffers unbounded array + uint meshVertexUVArrayIndex = meshInfo.m_bufferStartIndex + optionalBufferOffset++; + + // offset into the UV buffer for this vertex + uint uvOffset = meshInfo.m_uvOffset + (indices[i] * 8); + + // load the UV data + vertexData.m_uv += asfloat(RayTracingSceneSrg::m_meshBuffers[meshVertexUVArrayIndex].Load2(uvOffset)) * barycentrics[i]; + } + } + + vertexData.m_normal = normalize(vertexData.m_normal); + + return vertexData; +} \ No newline at end of file diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/SrgSemantics.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/SrgSemantics.azsli index c134a9d293..6d1b05d797 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/SrgSemantics.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/SrgSemantics.azsli @@ -64,3 +64,8 @@ ShaderResourceGroupSemantic SRG_RayTracingScene { FrequencyId = 1; }; + +ShaderResourceGroupSemantic SRG_RayTracingMaterial +{ + FrequencyId = 2; +}; diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracing.azshader b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracing.azshader index 804b534277c99e83211f6276d08f434bc053295d..746ae357ee6493061a828bd02010fab4308b1164 100644 GIT binary patch delta 430 zcmaF%n&s6SmJOP0LhSOBjs?zccCK6<6U6v&)1iup$rEiv7#Su%ob0f^`U|^iQGwb!_^A}19 zIb<`$<35)+Gh>Fzb8!x*?l*6|oftQ7e(;f%9j9TyfOy9^`GF$K<`=(iS#nf93#i<5 e{&>^m?kp{q28e4`PhV`xXwBjG??Z48(18HC4!d;# delta 414 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 literal 36232 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 M+%~eyrkm9MA3e>YGXMYp diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit.azshader b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit.azshader index c4a83a26dcc61d813d22d92798d7918b141877b3..0f42cd04d6971885c5d9acfd0d5665eee62d132a 100644 GIT binary patch delta 430 zcmezHn&rzImJP;iLhSOBjs?zccCK6<6U6v&)1iup$p@uG7#Sx0bl5!q;%3Il1}rR- z?@krn9KbP;d2)WS)Mmb->r9LZlNF^UH~%j=C(d#3V^5TA(UA$8&#SSr<1}y6&v#(6 zHs76k%aWrc)_9x9y&L;C@66<6WMrKD?)>Y?>$1CX>ZYx0PRev#asHkdG-JLH+z0shC7Tm{XApMalNq7;fb*1 g*2xFXYq2yiFn|JM_4Ku-jMf}}|Gub*GcYg!0Q-Em`Tzg` delta 492 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 VwsNenAw;~GzVgop{_cS1{{>MESCaq$ delta 7827 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 Rx-Q`=Vn@pQ;}D?1{{pwXlkfll diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit_null_0.azshadervariant b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit_null_0.azshadervariant index b7bdabeb4e9dfea986d9adf30d0f57c1225bedd9..d9d30c99f7c6609f3be3cfba81ec330469fb67fe 100644 GIT binary patch delta 16 XcmbQHJWY8+pCE_dzb`7{3=9kaH0}jO delta 16 XcmbQHJWY8+pCHFZ->{jC3=9kaF>D05 diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit_vulkan_0.azshadervariant b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingclosesthit_vulkan_0.azshadervariant index 1eeeffa8be8df2020435b46d5b5de152d8ece66b..e2f2a13ca3738fe19122583db01704237b19ea4a 100644 GIT binary patch literal 9452 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 h(SV%BmzPvt9Y1)pbK=9&-}`>& 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 delta 451 zcmaF&hUL|3mJPaWf^71Wjs?zccCK6<6U6v&)1ivVbw3?8vtQcGIN4z?%Vhm&qMH*q z<}pvM=aQQIpp#>>0?&D-$+|bBHrL!LVVpd{mu2&p+h$yo1J)T%j((OjSwK#2a>8qg z%@d^ExjEQwmhS$ubB^z3!CqE&Oq({>{d@7HpdlXqunu{1C+uuN|-Wqii5(U0S)5(5JR0P`Ti AlmGw# diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingmiss_dx12_0.azshadervariant b/Gems/Atom/Feature/Common/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridraytracingmiss_dx12_0.azshadervariant index 839efa72787ebf96ba18125c20d57da3997d6704..c144350c7ff1c7da6a32247b50d0e80f1d29ce31 100644 GIT binary patch delta 5097 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 A%m4rY delta 4676 zcmbtYc~q0vw*Nv#5&|D{AP_zVLu3qsL2ya}Mux-}m4a4n6DEl!5y4U@dJ~c`XoC$z z5Cz{21uRyuAcJDnM*>nnut2R+TIB&MczM*3$-xXpcCw;UbUNdfJH5W0*sG!FDludu4HGCb7qa(eswrWuV?;y51v` zL%TKODw^>G#`z-Wi5ZWvBHAkmdyR^g9Ak)oPJ#COvf1ux=du!!?{`1J@hk1WtI?S# zOq@-xhzaR;Qn`+eI@@2`Y+H1;-z#mOn(>SmXWaBgijxAr&Y=rlk?P^Nih{PJ`NRJ= zO`f-hSxkFWIR4r8BdvdcYfT(11R<2Yr}f{uvlYpV(gJpA zQZ_y}AHR`pe8OBtEmG>!DJ~<#Pc?sx_&7nCKI(&_((Ibjf^788%hZ{)dU{$JBXo`23pE zcMYY7m8Cgsl!zWi!RHZ53u^F@hT@!F{0GhW0tP+`!Rq5!p6OQNsIMYzAH%l4Fzw_B z=@OgtP-*v6X!o+WnAiyC}4sg>XNZm+K7G(FOJ^_`x$h{zEZi|qrMQfqttU33KgmUj<(tk=ftUZ;-eg#QrR4w zQcXKDbtvSa4HUc=DvLt1N8O;DThLf8PH7bm&PM?Sf-a!- zEm500s7M%!-s?&+2wwx+!Fgsa_`` z$8J!pf{1Nk(E*Wa3I7>z9TUgsY^j(SPnOO1X55eLvkM;v$>f3nZBVNmWYdW@;w3^E z(;*s}LdXF$*}Iui@!njj7DFTKi9_#GS4>(xAzC~tcO%^q&2e2vj(**KlyhygX}sxL z&-zoRMytjJ3K&{7De`;Dz8&M9B%QiGH)a~VzgRgvIJ`tBu-&B0%0gtxo7_yk8C7Ey z(5zZ}HD=vl-@3_|4SYD3<3cCY!(cmVFBaxkkn$^W-x5*2WS0&Wnm5hP8}3g`&$B4; zaWKz4DR%1mtZ8uK(Hx616bHYr!#P)Uhbp>PT?&uxB=fd8V7wYRUByNlrXVS7Q#A><^y7@8qvXD*%bY}}E9`fbNatqSanAbQu~^e zL@0m>8z&LU`gZT7qh!?+M`}rle{ZTvN$duN9TBOriCqAt!PFR`3l-BB#Ii|K%;5GK z&n^UxmbcjaG&CZ;palUNvEoM5rsf6Gq9u!k(S1;K@2?QiBzY*-WO zM)N|TU`vQLD0oEHBAqW^5P+4gV#gafH_k2mxF|SY9yBz1JRxL6X1gf6AXPzi#*!lB ztrBvagKJKhYb@1*K84gJB3)9FuGAonreHo?DKxmp6+hfMR9xQFb4E~J)l(%HI&-A! zNJZ07Rk@{)ird4Q&eV^SuYKJ!4w~^QqKz;#>&N_EXt@$uwlqU{Kq5cHm+Z|t6m#G` z`5~${Hk9B`W}MaEr&B54I89SMw@7q-W>!xdWxNV*C*i&BSIXU8ViCCVNu}PkGAYCHgyICxgi9t84BOC@;ra_kQ)w7u? z^vINIKYv$VwaXkUIeC`#j8{FEm*;fnjtf9$P`#$q64eqk>g%o*Lqm$l|zEGy2_badsWx_NzvmU%g#Vvz-Ze zf1Lh&Ao*_cou`>8carCLo2&3CEDmHAsVmI*3NMLSsL~fY2G+Xk3)9jHSB2LGHq?Tq zJP4ZdncDnOM0VCP0@lUlt%_5{VDoS4B7mVNu>b$$yxQ!ZG_t(^(-YI$+56M;Bc_4j zH$cz{y&eDp*gMt_2uHR!-BJ)ojMA1xu|5i~sKhzjUuLel7;&ki#@b4Vr)pa{1-nK0TzMG@`CaoSRt*YkR>Ur|23sXi-Lak8=R)F4nE+uDLz zQ2cMClrX#xUC~Jp!^_+AO{i@k8fm{2~+Q!Cw{H*sMQ>3 zQKS}Whf0)u7J+w4O7RE_(_5=ziLV+>KR3^s1}3I&9iP14Z<iRO&$7d%7 zLBP{6?n`z+&_@X&(xB0W$7KtGAgwH6@$vbEz!pJJ>qz;9bH~d=$j5_Q`GBEYte?0` zgirLp(bC$~LPt{CYhH>@tmTq?3qQA1KOa)?ZxxW1AH+Z|a2j!~zO9cO!+77~M`7v6}&ZEG=T>`z{c zgUzGYS|bB`XAMMSQ{%r>1{h|o$8i$jc-Bj9Vnf4}$ghw)&vj4YPhB6In6I9gRty4( z|112N4(C+W{aH`fx`3Xx_KRfR&;I?@I5ZIX&WJ1Lx~`IW4gs<28;xx!BYv&Xas7HypKzyvcJqO54-d>w+@A(vK7|GDNq)_z zO-n$^KZOtLa9@`wJ|^?AZzUDKMJF6%ND+r}CW~WGp0!sw;5Vsxqk$E<>uxS?h0&=cyYHgxoS-v%XSqailovL<1^e~YGghXK3ucJZDT zfv~X(btWS?vpV1P}bt;aX?@eeK8OC@AvLI@SFO2-;;KSgU1d zLu?3yxcw?<-dNH*DdL+erAe2ijC~JNAolLEvLQfq^;M(e%(Oz%G9?uD`i#mH$K`Sr zw1heu5qTxb=TJueyB1cXI^}o|-`IT%yJuok$nA_~(?I6kxk?_Oo5&S}PjxdrlT|~b zK)?moW%Ts?kUX(8FC%BWRfDbDzu)wDo3iuN?ysmj-R_-0R>qWk?~}rMp6nl)%!*vW zP$vNQLj~Cx9J%~}MH9mh$aA-Ab`DiVg8?9uIR(q24{Yz)p9}%EG>h_Qa&s( z21V#1lvs#!)U-@OQVK4Dt+uurlTmI%hq%n4`BTUh7nFP^#ZcuqW590t<}$mc5Ac+prhhkV0`o)p#w&jw4 z_sgBRY6b2&FJ+F0Usd8l{~{}(m3+L_CVf65D+8-ZU@)>`ba+S2T1ZVs)8>sdDP>V< z_e|6`hCkAVr{Xr4)$0-OXU_(@#SnJGq?#PV(3oga*k$$qzCX79pBu!|A6Bep&_~X8xe+LhCk&&B}>gYc65l zeG(^SO4*okNV@;UZ8o`tTxNDt+N&*7gzD%e;%bV3pqVK#kb6bdMFf?MoMy)DXQP-U zR4e^7o4@r-&uKtH_Su-WR+I_7fx<#ma;STCYypDr!^N8Id%JP*cK;AfDhb`F$veyi zj+YG;MtQ-&e$4J?i=8V5{z*s7%l>$bzJNnrrC<^j=7^C?jCi-*Nj7O~d?56L+6+pW zXqVbkqhf$1idNe6mKjk>gi^-|ylw2(4{&av)Osz~iv<2(p{P(^;LWJ-<|4iX(963@ r{N~lceGV^-)jmXS5nLRsref;bGAO0J;8Z|*RLpup`xe4OAMoR!@91)|gbyer>nzJ3LwwZbpHO9E2-Goh>EPmLtVZ~~DA=$j5_#L9& z_sQ)P{5T(zCB}Zg+2Q$dt;q?9Dz8hvYfM$9CQP8)^J+dKJ>uD?(o(CbGYQXcHW67a z2_TQN>_eZf#=3%n*-Hu<3jCDTe>}fXOGwN4x5JG5MlT`kb(7UvzY%W&b~BL=MQw{a zo<#hAc$2hMH$KVCm`8_-=$B=Oa+ueEEG^WDF8Qsp>{D`&CbmbGR?!G%o450Be3S@q z8rVghSY8BiZa}q1U&E0_kv*?&JD%JhirI(Bf`jj>f!4uG-XVXD#$kV*urE@%U9P+#;f>yIq=qIrJ0gY!GoLgKp59SE#@)owI_Afi z7BN2TaX;1&#Oz`^5$d*2(uo0H<`MP4*^^>Sy!OwX@?$)*m&!@l&*=va)l7(H!O57N z&g2GWorvxuVoXYKhBtyGo0nV`JkXtXHPfDC3g^UG!M9%XZ$0nFJMCs>*x}1w7MH|? zY0_=QY+{NUt_Y@aLZCb)=osB;!6|f~Dn|Z{AdkIAer#52bl8jN{l@HSHXbE=Ud##J zv6pSXW(%`uUlj|2Use4RyC#SS_g~j~K?GlhoEv_OnuBwTe!R@zy{+`-yXp6~xFv21 z6qsGD_jkPo2dz6|NlfS#&Q5a?rXj&>#n^9Q!`^p{ca}t;oH90YkNW4FCWD delta 1362 zcmZvbJ7`l;7{^c2#HJX9Yx8K6Cimv%MM$GyC{c^FzNpfgroL-it@x-)B|ZiRhpUsD z4&UJFrs7b=!HPq1b`*rtK^G~milA7k|NqUolEJ`_@0{=P|IRu0%UWZt@uF^QE^Yjn zdVEektLaj0`MbR9?3Si6;{O_XwP>t|ou~?^s8Y%T{FU`S*4qBt$FcXd#e1+0wC#Jb z{sUsFLnUA>5JyH5mV4&1S0Ahew3g#KQgCztHfM4g@J9g?mOjS&)dqKOVa*&5uPy`M z!}s<1i1I7jjd!UqEHyl+#S1fTtEy5ZNOO|~6ygbUpi}QzkO%k;yKz}H%f5tkTch&L zipaVZ3c9xx$4WY^=23f~)DO!D@6M}KV5#N@eb`4}8RuK3Rj=9t%VSc*yLK6C*8C6V z5if?C?QT;#+Y8y4&C+;31D1_g#CM3wXl9$wlslQK(Hew+8i1dNOv+LwiL0(;9t|+R z29&zHv=vw60e1kd8QZDX)Yzrhwmh+moA|JUfR7iI6?<@kwZmWta1bU`(AC?cee6vi zwf1TsACW#AcAxeIJ)g{FOiqC#maIdjDGYed>RfmKVr)=o3qydoVl1RU0sU=?*Az{mV)z&yt8Y2OUi)L|@)5jJHZpNum5 zEI0!=%5JuN%2#KoeICpL{v6z&z6*eUum44?XMy)+m~%<{cxJC|PWv1Dr%JO>PVv51 z!4+^BP=RmPy8G)w)u7fjFb{ahD{gFYwW2H^W<0pINx s^bf%P)b(modelLods.size() - 1); const Data::Instance& modelLod = modelLods[rayTracingLod]; - // setup a stream layout and shader input contract for the position and normal streams + // setup a stream layout and shader input contract for the vertex streams static const char* PositionSemantic = "POSITION"; static const char* NormalSemantic = "NORMAL"; - static const RHI::Format StreamFormat = RHI::Format::R32G32B32_FLOAT; + static const char* TangentSemantic = "TANGENT"; + static const char* BitangentSemantic = "BITANGENT"; + static const char* UVSemantic = "UV"; + static const RHI::Format PositionStreamFormat = RHI::Format::R32G32B32_FLOAT; + static const RHI::Format NormalStreamFormat = RHI::Format::R32G32B32_FLOAT; + static const RHI::Format TangentStreamFormat = RHI::Format::R32G32B32_FLOAT; + static const RHI::Format BitangentStreamFormat = RHI::Format::R32G32B32_FLOAT; + static const RHI::Format UVStreamFormat = RHI::Format::R32G32_FLOAT; RHI::InputStreamLayoutBuilder layoutBuilder; - layoutBuilder.AddBuffer()->Channel(PositionSemantic, StreamFormat); - layoutBuilder.AddBuffer()->Channel(NormalSemantic, StreamFormat); + layoutBuilder.AddBuffer()->Channel(PositionSemantic, PositionStreamFormat); + layoutBuilder.AddBuffer()->Channel(NormalSemantic, NormalStreamFormat); + layoutBuilder.AddBuffer()->Channel(UVSemantic, UVStreamFormat); + layoutBuilder.AddBuffer()->Channel(TangentSemantic, TangentStreamFormat); + layoutBuilder.AddBuffer()->Channel(BitangentSemantic, BitangentStreamFormat); RHI::InputStreamLayout inputStreamLayout = layoutBuilder.End(); RPI::ShaderInputContract::StreamChannelInfo positionStreamChannelInfo; positionStreamChannelInfo.m_semantic = RHI::ShaderSemantic(AZ::Name(PositionSemantic)); - positionStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(StreamFormat); + positionStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(PositionStreamFormat); RPI::ShaderInputContract::StreamChannelInfo normalStreamChannelInfo; normalStreamChannelInfo.m_semantic = RHI::ShaderSemantic(AZ::Name(NormalSemantic)); - normalStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(StreamFormat); + normalStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(NormalStreamFormat); + + RPI::ShaderInputContract::StreamChannelInfo tangentStreamChannelInfo; + tangentStreamChannelInfo.m_semantic = RHI::ShaderSemantic(AZ::Name(TangentSemantic)); + tangentStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(TangentStreamFormat); + + RPI::ShaderInputContract::StreamChannelInfo bitangentStreamChannelInfo; + bitangentStreamChannelInfo.m_semantic = RHI::ShaderSemantic(AZ::Name(BitangentSemantic)); + bitangentStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(BitangentStreamFormat); + + RPI::ShaderInputContract::StreamChannelInfo uvStreamChannelInfo; + uvStreamChannelInfo.m_semantic = RHI::ShaderSemantic(AZ::Name(UVSemantic)); + uvStreamChannelInfo.m_componentCount = RHI::GetFormatComponentCount(UVStreamFormat); + uvStreamChannelInfo.m_isOptional = true; RPI::ShaderInputContract shaderInputContract; shaderInputContract.m_streamChannels.emplace_back(positionStreamChannelInfo); shaderInputContract.m_streamChannels.emplace_back(normalStreamChannelInfo); + shaderInputContract.m_streamChannels.emplace_back(tangentStreamChannelInfo); + shaderInputContract.m_streamChannels.emplace_back(bitangentStreamChannelInfo); + shaderInputContract.m_streamChannels.emplace_back(uvStreamChannelInfo); // setup the raytracing data for each sub-mesh const size_t meshCount = modelLod->GetMeshes().size(); @@ -739,26 +765,6 @@ namespace AZ { const RPI::ModelLod::Mesh& mesh = modelLod->GetMeshes()[meshIndex]; - // retrieve vertex/index buffers - RPI::ModelLod::StreamBufferViewList streamBufferViews; - [[maybe_unused]] bool result = modelLod->GetStreamsForMesh(inputStreamLayout, streamBufferViews, shaderInputContract, meshIndex); - AZ_Assert(result, "Failed to retrieve mesh stream buffer views"); - - // note that the element count is the size of the entire buffer, even though this mesh may only - // occupy a portion of the vertex buffer. This is necessary since we are accessing it using - // a ByteAddressBuffer in the raytracing shaders and passing the byte offset to the shader in a constant buffer. - uint32_t vertexBufferByteCount = const_cast(streamBufferViews[0].GetBuffer())->GetDescriptor().m_byteCount; - RHI::BufferViewDescriptor vertexBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, vertexBufferByteCount); - - const RHI::IndexBufferView& indexBufferView = mesh.m_indexBufferView; - uint32_t indexElementSize = indexBufferView.GetIndexFormat() == RHI::IndexFormat::Uint16 ? 2 : 4; - uint32_t indexElementCount = (uint32_t)indexBufferView.GetBuffer()->GetDescriptor().m_byteCount / indexElementSize; - RHI::BufferViewDescriptor indexBufferDescriptor; - indexBufferDescriptor.m_elementOffset = 0; - indexBufferDescriptor.m_elementCount = indexElementCount; - indexBufferDescriptor.m_elementSize = indexElementSize; - indexBufferDescriptor.m_elementFormat = indexBufferView.GetIndexFormat() == RHI::IndexFormat::Uint16 ? RHI::Format::R16_UINT : RHI::Format::R32_UINT; - // retrieve the material Data::Instance material = mesh.m_material; @@ -769,31 +775,162 @@ namespace AZ material = materialAssignment.m_materialInstance; } - AZ::Color irradianceColor(1.0f, 1.0f, 1.0f, 1.0f); + // retrieve vertex/index buffers + RPI::ModelLod::StreamBufferViewList streamBufferViews; + [[maybe_unused]] bool result = modelLod->GetStreamsForMesh( + inputStreamLayout, + streamBufferViews, + shaderInputContract, + meshIndex, + materialAssignment.m_matModUvOverrides, + material->GetAsset()->GetMaterialTypeAsset()->GetUvNameMap()); + AZ_Assert(result, "Failed to retrieve mesh stream buffer views"); + + // note that the element count is the size of the entire buffer, even though this mesh may only + // occupy a portion of the vertex buffer. This is necessary since we are accessing it using + // a ByteAddressBuffer in the raytracing shaders and passing the byte offset to the shader in a constant buffer. + uint32_t positionBufferByteCount = const_cast(streamBufferViews[0].GetBuffer())->GetDescriptor().m_byteCount; + RHI::BufferViewDescriptor positionBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, positionBufferByteCount); + + uint32_t normalBufferByteCount = const_cast(streamBufferViews[1].GetBuffer())->GetDescriptor().m_byteCount; + RHI::BufferViewDescriptor normalBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, normalBufferByteCount); + + uint32_t tangentBufferByteCount = const_cast(streamBufferViews[2].GetBuffer())->GetDescriptor().m_byteCount; + RHI::BufferViewDescriptor tangentBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, tangentBufferByteCount); + + uint32_t bitangentBufferByteCount = const_cast(streamBufferViews[3].GetBuffer())->GetDescriptor().m_byteCount; + RHI::BufferViewDescriptor bitangentBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, bitangentBufferByteCount); + + uint32_t uvBufferByteCount = const_cast(streamBufferViews[4].GetBuffer())->GetDescriptor().m_byteCount; + RHI::BufferViewDescriptor uvBufferDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, uvBufferByteCount); + + const RHI::IndexBufferView& indexBufferView = mesh.m_indexBufferView; + uint32_t indexElementSize = indexBufferView.GetIndexFormat() == RHI::IndexFormat::Uint16 ? 2 : 4; + uint32_t indexElementCount = (uint32_t)indexBufferView.GetBuffer()->GetDescriptor().m_byteCount / indexElementSize; + RHI::BufferViewDescriptor indexBufferDescriptor; + indexBufferDescriptor.m_elementOffset = 0; + indexBufferDescriptor.m_elementCount = indexElementCount; + indexBufferDescriptor.m_elementSize = indexElementSize; + indexBufferDescriptor.m_elementFormat = indexBufferView.GetIndexFormat() == RHI::IndexFormat::Uint16 ? RHI::Format::R16_UINT : RHI::Format::R32_UINT; + + // set the SubMesh data to pass to the RayTracingFeatureProcessor, starting with vertex/index data + RayTracingFeatureProcessor::SubMesh subMesh; + subMesh.m_positionFormat = PositionStreamFormat; + subMesh.m_positionVertexBufferView = streamBufferViews[0]; + subMesh.m_positionShaderBufferView = const_cast(streamBufferViews[0].GetBuffer())->GetBufferView(positionBufferDescriptor); + + subMesh.m_normalFormat = NormalStreamFormat; + subMesh.m_normalVertexBufferView = streamBufferViews[1]; + subMesh.m_normalShaderBufferView = const_cast(streamBufferViews[1].GetBuffer())->GetBufferView(normalBufferDescriptor); + + subMesh.m_tangentFormat = TangentStreamFormat; + subMesh.m_tangentVertexBufferView = streamBufferViews[2]; + subMesh.m_tangentShaderBufferView = const_cast(streamBufferViews[2].GetBuffer())->GetBufferView(tangentBufferDescriptor); + + subMesh.m_bitangentFormat = BitangentStreamFormat; + subMesh.m_bitangentVertexBufferView = streamBufferViews[3]; + subMesh.m_bitangentShaderBufferView = const_cast(streamBufferViews[3].GetBuffer())->GetBufferView(bitangentBufferDescriptor); + + if (uvBufferByteCount > 0) + { + subMesh.m_bufferFlags |= RayTracingSubMeshBufferFlags::UV; + subMesh.m_uvFormat = UVStreamFormat; + subMesh.m_uvVertexBufferView = streamBufferViews[4]; + subMesh.m_uvShaderBufferView = const_cast(streamBufferViews[4].GetBuffer())->GetBufferView(uvBufferDescriptor); + } + + subMesh.m_indexBufferView = mesh.m_indexBufferView; + subMesh.m_indexShaderBufferView = const_cast(mesh.m_indexBufferView.GetBuffer())->GetBufferView(indexBufferDescriptor); + + // add material data if (material) { + // irradiance color RPI::MaterialPropertyIndex propertyIndex = material->FindPropertyIndex(AZ::Name("irradiance.color")); if (propertyIndex.IsValid()) { - irradianceColor = material->GetPropertyValue(propertyIndex); + subMesh.m_irradianceColor = material->GetPropertyValue(propertyIndex); } propertyIndex = material->FindPropertyIndex(AZ::Name("irradiance.factor")); if (propertyIndex.IsValid()) { - irradianceColor *= material->GetPropertyValue(propertyIndex); + subMesh.m_irradianceColor *= material->GetPropertyValue(propertyIndex); + } + + // base color + propertyIndex = material->FindPropertyIndex(AZ::Name("baseColor.color")); + if (propertyIndex.IsValid()) + { + subMesh.m_baseColor = material->GetPropertyValue(propertyIndex); + } + + propertyIndex = material->FindPropertyIndex(AZ::Name("baseColor.factor")); + if (propertyIndex.IsValid()) + { + subMesh.m_baseColor *= material->GetPropertyValue(propertyIndex); + } + + // metallic + propertyIndex = material->FindPropertyIndex(AZ::Name("metallic.factor")); + if (propertyIndex.IsValid()) + { + subMesh.m_metallicFactor = material->GetPropertyValue(propertyIndex); + } + + // roughness + propertyIndex = material->FindPropertyIndex(AZ::Name("roughness.factor")); + if (propertyIndex.IsValid()) + { + subMesh.m_roughnessFactor = material->GetPropertyValue(propertyIndex); + } + + // textures + propertyIndex = material->FindPropertyIndex(AZ::Name("baseColor.textureMap")); + if (propertyIndex.IsValid()) + { + Data::Instance image = material->GetPropertyValue>(propertyIndex); + if (image.get()) + { + subMesh.m_textureFlags |= RayTracingSubMeshTextureFlags::BaseColor; + subMesh.m_baseColorImageView = image->GetImageView(); + } + } + + propertyIndex = material->FindPropertyIndex(AZ::Name("normal.textureMap")); + if (propertyIndex.IsValid()) + { + Data::Instance image = material->GetPropertyValue>(propertyIndex); + if (image.get()) + { + subMesh.m_textureFlags |= RayTracingSubMeshTextureFlags::Normal; + subMesh.m_normalImageView = image->GetImageView(); + } + } + + propertyIndex = material->FindPropertyIndex(AZ::Name("metallic.textureMap")); + if (propertyIndex.IsValid()) + { + Data::Instance image = material->GetPropertyValue>(propertyIndex); + if (image.get()) + { + subMesh.m_textureFlags |= RayTracingSubMeshTextureFlags::Metallic; + subMesh.m_metallicImageView = image->GetImageView(); + } + } + + propertyIndex = material->FindPropertyIndex(AZ::Name("roughness.textureMap")); + if (propertyIndex.IsValid()) + { + Data::Instance image = material->GetPropertyValue>(propertyIndex); + if (image.get()) + { + subMesh.m_textureFlags |= RayTracingSubMeshTextureFlags::Roughness; + subMesh.m_roughnessImageView = image->GetImageView(); + } } } - RayTracingFeatureProcessor::SubMesh subMesh; - subMesh.m_vertexFormat = StreamFormat; - subMesh.m_positionVertexBufferView = streamBufferViews[0]; - subMesh.m_positionShaderBufferView = const_cast(streamBufferViews[0].GetBuffer())->GetBufferView(vertexBufferDescriptor); - subMesh.m_normalVertexBufferView = streamBufferViews[1]; - subMesh.m_normalShaderBufferView = const_cast(streamBufferViews[1].GetBuffer())->GetBufferView(vertexBufferDescriptor); - subMesh.m_indexBufferView = mesh.m_indexBufferView; - subMesh.m_indexShaderBufferView = const_cast(mesh.m_indexBufferView.GetBuffer())->GetBufferView(indexBufferDescriptor); - subMesh.m_irradianceColor = irradianceColor; subMeshes.push_back(subMesh); } diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingAccelerationStructurePass.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingAccelerationStructurePass.cpp index 2bb2fa2ac2..92cd41b4e8 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingAccelerationStructurePass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingAccelerationStructurePass.cpp @@ -115,11 +115,11 @@ namespace AZ } } - // update and compile the RayTracingSceneSrg + // update and compile the RayTracingSceneSrg and RayTracingMaterialSrg // Note: the timing of this update is very important, it needs to be updated after the TLAS is allocated so it can // be set on the RayTracingSceneSrg for this frame, and the ray tracing mesh data in the RayTracingSceneSrg must // exactly match the TLAS. Any mismatch in this data may result in a TDR. - rayTracingFeatureProcessor->UpdateRayTracingSceneSrg(); + rayTracingFeatureProcessor->UpdateRayTracingSrgs(); } } diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp index c4e9306dc9..c28ac32381 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp @@ -71,6 +71,13 @@ namespace AZ AZ_Assert(rayTracingSceneSrgAsset.IsReady(), "Failed to load RayTracingSceneSrg asset"); m_rayTracingSceneSrg = RPI::ShaderResourceGroup::Create(rayTracingSceneSrgAsset); + + // load the RayTracingMaterialSrg asset + Data::Asset rayTracingMaterialSrgAsset = + RPI::AssetUtils::LoadAssetByProductPath("shaderlib/atom/features/raytracing/raytracingmaterialsrg_raytracingmaterialsrg.azsrg", RPI::AssetUtils::TraceLevel::Error); + AZ_Assert(rayTracingMaterialSrgAsset.IsReady(), "Failed to load RayTracingMaterialSrg asset"); + + m_rayTracingMaterialSrg = RPI::ShaderResourceGroup::Create(rayTracingMaterialSrgAsset); } void RayTracingFeatureProcessor::SetMesh(const ObjectId objectId, const SubMeshVector& subMeshes) @@ -104,7 +111,7 @@ namespace AZ RHI::RayTracingBlasDescriptor blasDescriptor; blasDescriptor.Build() ->Geometry() - ->VertexFormat(subMesh.m_vertexFormat) + ->VertexFormat(subMesh.m_positionFormat) ->VertexBuffer(subMesh.m_positionVertexBufferView) ->IndexBuffer(subMesh.m_indexBufferView) ; @@ -124,6 +131,7 @@ namespace AZ m_subMeshCount += aznumeric_cast(subMeshes.size()); m_meshInfoBufferNeedsUpdate = true; + m_materialInfoBufferNeedsUpdate = true; } void RayTracingFeatureProcessor::RemoveMesh(const ObjectId objectId) @@ -142,6 +150,7 @@ namespace AZ } m_meshInfoBufferNeedsUpdate = true; + m_materialInfoBufferNeedsUpdate = true; } void RayTracingFeatureProcessor::SetMeshTransform(const ObjectId objectId, const AZ::Transform transform, const AZ::Vector3 nonUniformScale) @@ -162,14 +171,14 @@ namespace AZ m_meshInfoBufferNeedsUpdate = true; } - void RayTracingFeatureProcessor::UpdateRayTracingSceneSrg() + void RayTracingFeatureProcessor::UpdateRayTracingSrgs() { if (!m_tlas->GetTlasBuffer()) { return; } - if (m_rayTracingSceneSrg->IsQueuedForCompile()) + if (m_rayTracingSceneSrg->IsQueuedForCompile() || m_rayTracingMaterialSrg->IsQueuedForCompile()) { //[GFX TODO][ATOM-14792] AtomSampleViewer: Reset scene and feature processors before switching to sample return; @@ -178,7 +187,144 @@ namespace AZ // update the mesh info buffer with the latest ray tracing enabled meshes UpdateMeshInfoBuffer(); + // update the material info buffer with the latest ray tracing enabled meshes + UpdateMaterialInfoBuffer(); + // update the RayTracingSceneSrg + UpdateRayTracingSceneSrg(); + + // update the RayTracingMaterialSrg + UpdateRayTracingMaterialSrg(); + } + + void RayTracingFeatureProcessor::UpdateMeshInfoBuffer() + { + if (m_meshInfoBufferNeedsUpdate && (m_subMeshCount > 0)) + { + TransformServiceFeatureProcessor* transformFeatureProcessor = GetParentScene()->GetFeatureProcessor(); + + AZStd::vector meshInfos; + meshInfos.reserve(m_subMeshCount); + + uint32_t newMeshByteCount = m_subMeshCount * sizeof(MeshInfo); + + if (m_meshInfoBuffer == nullptr) + { + // allocate the MeshInfo structured buffer + RPI::CommonBufferDescriptor desc; + desc.m_poolType = RPI::CommonBufferPoolType::ReadOnly; + desc.m_bufferName = "RayTracingMeshInfo"; + desc.m_byteCount = newMeshByteCount; + desc.m_elementSize = sizeof(MeshInfo); + m_meshInfoBuffer = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); + } + else if (m_meshInfoBuffer->GetBufferSize() < newMeshByteCount) + { + // resize for the new sub-mesh count + m_meshInfoBuffer->Resize(newMeshByteCount); + } + + // keep track of the start index of the buffers for each mesh, this is put into the MeshInfo + // entry for each mesh so it knows where to find the start of its buffers in the unbounded array + uint32_t bufferStartIndex = 0; + + for (const auto& mesh : m_meshes) + { + AZ::Transform meshTransform = transformFeatureProcessor->GetTransformForId(TransformServiceFeatureProcessorInterface::ObjectId(mesh.first)); + AZ::Transform noScaleTransform = meshTransform; + noScaleTransform.ExtractScale(); + AZ::Matrix3x3 rotationMatrix = Matrix3x3::CreateFromTransform(noScaleTransform); + rotationMatrix = rotationMatrix.GetInverseFull().GetTranspose(); + + const RayTracingFeatureProcessor::SubMeshVector& subMeshes = mesh.second.m_subMeshes; + for (const auto& subMesh : subMeshes) + { + MeshInfo meshInfo; + meshInfo.m_indexOffset = subMesh.m_indexBufferView.GetByteOffset(); + meshInfo.m_positionOffset = subMesh.m_positionVertexBufferView.GetByteOffset(); + meshInfo.m_normalOffset = subMesh.m_normalVertexBufferView.GetByteOffset(); + meshInfo.m_tangentOffset = subMesh.m_tangentVertexBufferView.GetByteOffset(); + meshInfo.m_bitangentOffset = subMesh.m_bitangentVertexBufferView.GetByteOffset(); + + if (RHI::CheckBitsAll(subMesh.m_bufferFlags, RayTracingSubMeshBufferFlags::UV)) + { + meshInfo.m_uvOffset = subMesh.m_uvVertexBufferView.GetByteOffset(); + } + + subMesh.m_irradianceColor.StoreToFloat4(meshInfo.m_irradianceColor.data()); + rotationMatrix.StoreToRowMajorFloat9(meshInfo.m_worldInvTranspose.data()); + meshInfo.m_bufferFlags = subMesh.m_bufferFlags; + meshInfo.m_bufferStartIndex = bufferStartIndex; + + // add the count of buffers present in this subMesh to the start index for the next subMesh + // note that the Index, Position, Normal, Tangent, and Bitangent buffers are always counted since they are guaranteed + static const uint32_t RayTracingSubMeshFixedStreamCount = 5; + bufferStartIndex += (RayTracingSubMeshFixedStreamCount + RHI::CountBitsSet(aznumeric_cast(meshInfo.m_bufferFlags))); + + meshInfos.emplace_back(meshInfo); + } + } + + m_meshInfoBuffer->UpdateData(meshInfos.data(), newMeshByteCount); + m_meshInfoBufferNeedsUpdate = false; + } + } + + void RayTracingFeatureProcessor::UpdateMaterialInfoBuffer() + { + if (m_materialInfoBufferNeedsUpdate && (m_subMeshCount > 0)) + { + AZStd::vector materialInfos; + materialInfos.reserve(m_subMeshCount); + + uint32_t newMaterialByteCount = m_subMeshCount * sizeof(MaterialInfo); + + if (m_materialInfoBuffer == nullptr) + { + // allocate the MaterialInfo structured buffer + RPI::CommonBufferDescriptor desc; + desc.m_poolType = RPI::CommonBufferPoolType::ReadOnly; + desc.m_bufferName = "RayTracingMaterialInfo"; + desc.m_byteCount = newMaterialByteCount; + desc.m_elementSize = sizeof(MaterialInfo); + m_materialInfoBuffer = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); + } + else if (m_materialInfoBuffer->GetBufferSize() < newMaterialByteCount) + { + // resize for the new sub-mesh count + m_materialInfoBuffer->Resize(newMaterialByteCount); + } + + // keep track of the start index of the textures for each mesh, this is put into the MaterialInfo + // entry for each mesh so it knows where to find the start of its textures in the unbounded array + uint32_t textureStartIndex = 0; + + for (const auto& mesh : m_meshes) + { + const RayTracingFeatureProcessor::SubMeshVector& subMeshes = mesh.second.m_subMeshes; + for (const auto& subMesh : subMeshes) + { + MaterialInfo materialInfo; + subMesh.m_baseColor.StoreToFloat4(materialInfo.m_baseColor.data()); + materialInfo.m_metallicFactor = subMesh.m_metallicFactor; + materialInfo.m_roughnessFactor = subMesh.m_roughnessFactor; + materialInfo.m_textureFlags = subMesh.m_textureFlags; + materialInfo.m_textureStartIndex = textureStartIndex; + + // add the count of textures present in this subMesh to the start index for the next subMesh + textureStartIndex += RHI::CountBitsSet(aznumeric_cast(materialInfo.m_textureFlags)); + + materialInfos.emplace_back(materialInfo); + } + } + + m_materialInfoBuffer->UpdateData(materialInfos.data(), newMaterialByteCount); + m_materialInfoBufferNeedsUpdate = false; + } + } + + void RayTracingFeatureProcessor::UpdateRayTracingSceneSrg() + { const RHI::ShaderResourceGroupLayout* srgLayout = m_rayTracingSceneSrg->GetLayout(); RHI::ShaderInputImageIndex imageIndex; RHI::ShaderInputBufferIndex bufferIndex; @@ -272,11 +418,18 @@ namespace AZ const SubMeshVector& subMeshes = mesh.second.m_subMeshes; for (const auto& subMesh : subMeshes) { - // add the index, position, and normal buffers for this sub-mesh to the mesh buffer list, this will - // go into the shader as an unbounded array in the Srg + // add the stream buffers for this sub-mesh to the mesh buffer list, + // this is sent to the shader as an unbounded array in the Srg meshBuffers.push_back(subMesh.m_indexShaderBufferView.get()); meshBuffers.push_back(subMesh.m_positionShaderBufferView.get()); meshBuffers.push_back(subMesh.m_normalShaderBufferView.get()); + meshBuffers.push_back(subMesh.m_tangentShaderBufferView.get()); + meshBuffers.push_back(subMesh.m_bitangentShaderBufferView.get()); + + if (RHI::CheckBitsAll(subMesh.m_bufferFlags, RayTracingSubMeshBufferFlags::UV)) + { + meshBuffers.push_back(subMesh.m_uvShaderBufferView.get()); + } } } @@ -287,58 +440,53 @@ namespace AZ m_rayTracingSceneSrg->Compile(); } - void RayTracingFeatureProcessor::UpdateMeshInfoBuffer() + void RayTracingFeatureProcessor::UpdateRayTracingMaterialSrg() { - if (m_meshInfoBufferNeedsUpdate && (m_subMeshCount > 0)) + const RHI::ShaderResourceGroupLayout* srgLayout = m_rayTracingMaterialSrg->GetLayout(); + RHI::ShaderInputImageIndex imageIndex; + RHI::ShaderInputBufferIndex bufferIndex; + RHI::ShaderInputConstantIndex constantIndex; + + bufferIndex = srgLayout->FindShaderInputBufferIndex(AZ::Name("m_materialInfo")); + m_rayTracingMaterialSrg->SetBufferView(bufferIndex, m_materialInfoBuffer->GetBufferView()); + + if (m_subMeshCount) { - TransformServiceFeatureProcessor* transformFeatureProcessor = GetParentScene()->GetFeatureProcessor(); - - AZStd::vector meshInfos; - meshInfos.reserve(m_subMeshCount); - - uint32_t newMeshByteCount = m_subMeshCount * sizeof(MeshInfo); - - if (m_meshInfoBuffer == nullptr) - { - // allocate the MeshInfo structured buffer - RPI::CommonBufferDescriptor desc; - desc.m_poolType = RPI::CommonBufferPoolType::ReadOnly; - desc.m_bufferName = "RayTracingMeshInfo"; - desc.m_byteCount = newMeshByteCount; - desc.m_elementSize = sizeof(MeshInfo); - m_meshInfoBuffer = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); - } - else if (m_meshInfoBuffer->GetBufferSize() < newMeshByteCount) - { - // resize for the new sub-mesh count - m_meshInfoBuffer->Resize(newMeshByteCount); - } - + AZStd::vector materialTextures; for (const auto& mesh : m_meshes) { - AZ::Transform meshTransform = transformFeatureProcessor->GetTransformForId(TransformServiceFeatureProcessorInterface::ObjectId(mesh.first)); - AZ::Transform noScaleTransform = meshTransform; - noScaleTransform.ExtractScale(); - AZ::Matrix3x3 rotationMatrix = Matrix3x3::CreateFromTransform(noScaleTransform); - rotationMatrix = rotationMatrix.GetInverseFull().GetTranspose(); - - const RayTracingFeatureProcessor::SubMeshVector& subMeshes = mesh.second.m_subMeshes; + const SubMeshVector& subMeshes = mesh.second.m_subMeshes; for (const auto& subMesh : subMeshes) { - MeshInfo meshInfo; - meshInfo.m_indexOffset = subMesh.m_indexBufferView.GetByteOffset(); - meshInfo.m_positionOffset = subMesh.m_positionVertexBufferView.GetByteOffset(); - meshInfo.m_normalOffset = subMesh.m_normalVertexBufferView.GetByteOffset(); - subMesh.m_irradianceColor.StoreToFloat4(meshInfo.m_irradianceColor.data()); - rotationMatrix.StoreToRowMajorFloat9(meshInfo.m_worldInvTranspose.data()); + // add the baseColor, normal, metallic, and roughness images for this sub-mesh to the material texture list, + // this is sent to the shader as an unbounded array in the Srg + if (RHI::CheckBitsAll(subMesh.m_textureFlags, RayTracingSubMeshTextureFlags::BaseColor)) + { + materialTextures.push_back(subMesh.m_baseColorImageView.get()); + } - meshInfos.emplace_back(meshInfo); + if (RHI::CheckBitsAll(subMesh.m_textureFlags, RayTracingSubMeshTextureFlags::Normal)) + { + materialTextures.push_back(subMesh.m_normalImageView.get()); + } + + if (RHI::CheckBitsAll(subMesh.m_textureFlags, RayTracingSubMeshTextureFlags::Metallic)) + { + materialTextures.push_back(subMesh.m_metallicImageView.get()); + } + + if (RHI::CheckBitsAll(subMesh.m_textureFlags, RayTracingSubMeshTextureFlags::Roughness)) + { + materialTextures.push_back(subMesh.m_roughnessImageView.get()); + } } } - m_meshInfoBuffer->UpdateData(meshInfos.data(), newMeshByteCount); - m_meshInfoBufferNeedsUpdate = false; + RHI::ShaderInputImageUnboundedArrayIndex textureUnboundedArrayIndex = srgLayout->FindShaderInputImageUnboundedArrayIndex(AZ::Name("m_materialTextures")); + m_rayTracingMaterialSrg->SetImageViewUnboundedArray(textureUnboundedArrayIndex, materialTextures); } + + m_rayTracingMaterialSrg->Compile(); } } } diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h index f317f1c096..d89b61b2f9 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,28 @@ namespace AZ { namespace Render { + static const uint32_t RayTracingGlobalSrgBindingSlot = 0; + static const uint32_t RayTracingSceneSrgBindingSlot = 1; + static const uint32_t RayTracingMaterialSrgBindingSlot = 2; + + enum class RayTracingSubMeshBufferFlags : uint32_t + { + None = 0, + + UV = AZ_BIT(0) + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(AZ::Render::RayTracingSubMeshBufferFlags); + + enum class RayTracingSubMeshTextureFlags : uint32_t + { + None = 0, + BaseColor = AZ_BIT(0), + Normal = AZ_BIT(1), + Metallic = AZ_BIT(2), + Roughness = AZ_BIT(3) + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(AZ::Render::RayTracingSubMeshTextureFlags); + //! This feature processor manages ray tracing data for a Scene class RayTracingFeatureProcessor : public RPI::FeatureProcessor @@ -42,20 +65,53 @@ namespace AZ //! Contains data for a single sub-mesh struct SubMesh { - // vertex/index buffer data - RHI::Format m_vertexFormat = RHI::Format::Unknown; + // vertex streams + RHI::Format m_positionFormat = RHI::Format::Unknown; RHI::StreamBufferView m_positionVertexBufferView; RHI::Ptr m_positionShaderBufferView; + + RHI::Format m_normalFormat = RHI::Format::Unknown; RHI::StreamBufferView m_normalVertexBufferView; RHI::Ptr m_normalShaderBufferView; + + RHI::Format m_tangentFormat = RHI::Format::Unknown; + RHI::StreamBufferView m_tangentVertexBufferView; + RHI::Ptr m_tangentShaderBufferView; + + RHI::Format m_bitangentFormat = RHI::Format::Unknown; + RHI::StreamBufferView m_bitangentVertexBufferView; + RHI::Ptr m_bitangentShaderBufferView; + + RHI::Format m_uvFormat = RHI::Format::Unknown; + RHI::StreamBufferView m_uvVertexBufferView; + RHI::Ptr m_uvShaderBufferView; + + // index buffer RHI::IndexBufferView m_indexBufferView; RHI::Ptr m_indexShaderBufferView; + // vertex buffer usage flags + RayTracingSubMeshBufferFlags m_bufferFlags = RayTracingSubMeshBufferFlags::None; + // color of the bounced light from this sub-mesh - AZ::Color m_irradianceColor; + AZ::Color m_irradianceColor = AZ::Color(1.0f); // ray tracing Blas RHI::Ptr m_blas; + + // material data + AZ::Color m_baseColor = AZ::Color(0.0f); + float m_metallicFactor = 0.0f; + float m_roughnessFactor = 0.0f; + + // material texture usage flags + RayTracingSubMeshTextureFlags m_textureFlags = RayTracingSubMeshTextureFlags::None; + + // material textures + RHI::Ptr m_baseColorImageView; + RHI::Ptr m_normalImageView; + RHI::Ptr m_metallicImageView; + RHI::Ptr m_roughnessImageView; }; using SubMeshVector = AZStd::vector; @@ -98,6 +154,9 @@ namespace AZ //! Retrieves the RayTracingSceneSrg Data::Instance GetRayTracingSceneSrg() const { return m_rayTracingSceneSrg; } + //! Retrieves the RayTracingMaterialSrg + Data::Instance GetRayTracingMaterialSrg() const { return m_rayTracingMaterialSrg; } + //! Retrieves the RayTracingTlas const RHI::Ptr& GetTlas() const { return m_tlas; } RHI::Ptr& GetTlas() { return m_tlas; } @@ -118,14 +177,20 @@ namespace AZ //! Retrieves the GPU buffer containing information for all ray tracing meshes. const Data::Instance GetMeshInfoBuffer() const { return m_meshInfoBuffer; } - //! Updates the RayTracingSceneSrg, called after the TLAS is allocated in the RayTracingAccelerationStructurePass - void UpdateRayTracingSceneSrg(); + //! Retrieves the GPU buffer containing information for all ray tracing materials. + const Data::Instance GetMaterialInfoBuffer() const { return m_materialInfoBuffer; } + + //! Updates the RayTracingSceneSrg and RayTracingMaterialSrg, called after the TLAS is allocated in the RayTracingAccelerationStructurePass + void UpdateRayTracingSrgs(); private: AZ_DISABLE_COPY_MOVE(RayTracingFeatureProcessor); void UpdateMeshInfoBuffer(); + void UpdateMaterialInfoBuffer(); + void UpdateRayTracingSceneSrg(); + void UpdateRayTracingMaterialSrg(); // flag indicating if RayTracing is enabled, currently based on device support bool m_rayTracingEnabled = false; @@ -143,6 +208,9 @@ namespace AZ // ray tracing scene Srg Data::Instance m_rayTracingSceneSrg; + // ray tracing material Srg + Data::Instance m_rayTracingMaterialSrg; + // current revision number of ray tracing data uint32_t m_revision = 0; @@ -158,18 +226,43 @@ namespace AZ // structure for data in the m_meshInfoBuffer, shaders that use the buffer must match this type struct MeshInfo { - uint32_t m_indexOffset; - uint32_t m_positionOffset; - uint32_t m_normalOffset; + uint32_t m_indexOffset; + uint32_t m_positionOffset; + uint32_t m_normalOffset; + uint32_t m_tangentOffset; + uint32_t m_bitangentOffset; + uint32_t m_uvOffset; + float m_padding0[2]; + AZStd::array m_irradianceColor; // float4 AZStd::array m_worldInvTranspose; // float3x3 + float m_padding1[1]; + + RayTracingSubMeshBufferFlags m_bufferFlags = RayTracingSubMeshBufferFlags::None; + uint32_t m_bufferStartIndex = 0; }; // buffer containing a MeshInfo for each sub-mesh Data::Instance m_meshInfoBuffer; - // flag indicating we need to update the mesh info GPU buffer + // structure for data in the m_materialInfoBuffer, shaders that use the buffer must match this type + struct MaterialInfo + { + AZStd::array m_baseColor; // float4 + float m_metallicFactor = 0.0f; + float m_roughnessFactor = 0.0f; + RayTracingSubMeshTextureFlags m_textureFlags = RayTracingSubMeshTextureFlags::None; + uint32_t m_textureStartIndex = 0; + }; + + // buffer containing a MaterialInfo for each sub-mesh + Data::Instance m_materialInfoBuffer; + + // flag indicating we need to update the meshInfo buffer bool m_meshInfoBufferNeedsUpdate = false; + + // flag indicating we need to update the materialInfo buffer + bool m_materialInfoBufferNeedsUpdate = false; }; } } diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp index 2c0c7e986e..0fbe7dbc48 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp @@ -111,7 +111,6 @@ namespace AZ AZ_Assert(m_globalPipelineState, "Failed to acquire ray tracing global pipeline state"); // create global srg - static const uint32_t RayTracingGlobalSrgBindingSlot = 0; Data::Asset globalSrgAsset = m_rayGenerationShader->FindShaderResourceGroupAsset(RayTracingGlobalSrgBindingSlot); AZ_Error("PassSystem", globalSrgAsset.GetId().IsValid(), "RayTracingPass [%s] Failed to find RayTracingGlobalSrg asset", GetPathName().GetCStr()); AZ_Error("PassSystem", globalSrgAsset.IsReady(), "RayTracingPass [%s] asset is not loaded for shader", GetPathName().GetCStr()); @@ -120,10 +119,13 @@ namespace AZ AZ_Assert(m_shaderResourceGroup, "RayTracingPass [%s]: Failed to create RayTracingGlobalSrg", GetPathName().GetCStr()); RPI::PassUtils::BindDataMappingsToSrg(m_passDescriptor, m_shaderResourceGroup.get()); - // check to see if the shader requires a ViewSrg + // check to see if the shader requires the View and RayTracingMaterial Srgs Data::Asset viewSrgAsset = m_rayGenerationShader->FindShaderResourceGroupAsset(RPI::SrgBindingSlot::View); m_requiresViewSrg = viewSrgAsset.GetId().IsValid(); + Data::Asset rayTracingMaterialSrgAsset = m_rayGenerationShader->FindShaderResourceGroupAsset(RayTracingMaterialSrgBindingSlot); + m_requiresRayTracingMaterialSrg = rayTracingMaterialSrgAsset.GetId().IsValid(); + // build the ray tracing pipeline state descriptor RHI::RayTracingPipelineStateDescriptor descriptor; descriptor.Build() @@ -298,6 +300,11 @@ namespace AZ } } + if (m_requiresRayTracingMaterialSrg) + { + shaderResourceGroups.push_back(rayTracingFeatureProcessor->GetRayTracingMaterialSrg()->GetRHIShaderResourceGroup()); + } + dispatchRaysItem.m_shaderResourceGroupCount = aznumeric_cast(shaderResourceGroups.size()); dispatchRaysItem.m_shaderResourceGroups = shaderResourceGroups.data(); dispatchRaysItem.m_rayTracingPipelineState = m_rayTracingPipelineState.get(); diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h index 935d034513..6ad082e894 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.h @@ -76,6 +76,7 @@ namespace AZ RHI::ConstPtr m_globalPipelineState; RHI::Ptr m_rayTracingShaderTable; bool m_requiresViewSrg = false; + bool m_requiresRayTracingMaterialSrg = false; }; } // namespace RPI } // namespace AZ From ada73c2834b47fcbb90b3a36f5cae26243a07c1e Mon Sep 17 00:00:00 2001 From: zsolleci Date: Wed, 26 May 2021 12:22:18 -0500 Subject: [PATCH 06/53] T92567323 & T92569017 completed, suite updated --- ...nt_AddRemoveParameter_ActionsSuccessful.py | 133 ++++++++++++++++++ .../scripting/TestSuite_Periodic.py | 12 ++ 2 files changed, 145 insertions(+) create mode 100644 AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py diff --git a/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py b/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py new file mode 100644 index 0000000000..a587354985 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py @@ -0,0 +1,133 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + + +# fmt: off +class Tests(): + new_event_created = ("Successfully created a new event", "Failed to create a new event") + child_event_created = ("Successfully created Child Event", "Failed to create Child Event") + file_saved = ("Successfully saved event asset", "Failed to save event asset") + parameter_created = ("Successfully added parameter", "Failed to add parameter") + parameter_removed = ("Successfully removed parameter", "Failed to remove parameter") +# fmt: on + + +def ScriptEvent_AddRemoveParameter_ActionsSuccessful(): + """ + Summary: + Parameter can be removed from a Script Event method + + Expected Behavior: + Upon saving the updated .scriptevents asset the removed paramenter should no longer be present on the Script Event + + Test Steps: + 1) Open Asset Editor + 2) Get Asset Editor Qt object + 3) Create new Script Event Asset + 4) Add Parameter to Event + 5) Verify Parameter exists + 6) Remove Parameter from Event + 7) Verify Parameter has been removed + + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. + + :return: None + """ + import os + from PySide2 import QtWidgets + + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + import editor_python_test_tools.pyside_utils as pyside_utils + + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.legacy.general as general + + GENERAL_WAIT = 1.0 # seconds + FILE_PATH = os.path.join("AutomatedTesting", "ScriptCanvas", "test_file.scriptevent") + QtObject = object + + def create_script_event(asset_editor: QtObject, file_path: str) -> None: + action = pyside_utils.find_child_by_pattern(menu_bar, {"type": QtWidgets.QAction, "text": "Script Events"}) + action.trigger() + result = helper.wait_for_condition( + lambda: container.findChild(QtWidgets.QFrame, "Events") is not None, 3 * GENERAL_WAIT + ) + Report.result(Tests.new_event_created, result) + + # Add new child event + add_event = container.findChild(QtWidgets.QFrame, "Events").findChild(QtWidgets.QToolButton, "") + add_event.click() + result = helper.wait_for_condition( + lambda: asset_editor.findChild(QtWidgets.QFrame, "EventName") is not None, GENERAL_WAIT + ) + Report.result(Tests.child_event_created, result) + # Save the Script Event file + editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path) + + # Verify if file is created + result = helper.wait_for_condition(lambda: os.path.exists(file_path), 3 * GENERAL_WAIT) + Report.result(Tests.file_saved, result) + + def create_parameter(file_path: str) -> None: + add_param = container.findChild(QtWidgets.QFrame, "Parameters").findChild(QtWidgets.QToolButton, "") + add_param.click() + result = helper.wait_for_condition( + lambda: asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is not None, GENERAL_WAIT + ) + Report.result(Tests.parameter_created, result) + editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path) + + def remove_parameter(file_path: str) -> None: + remove_param = container.findChild(QtWidgets.QFrame, "[0]").findChild(QtWidgets.QToolButton, "") + remove_param.click() + result = helper.wait_for_condition( + lambda: asset_editor_widget.findChild(QtWidgets.QFrame, "[0]") is None, GENERAL_WAIT + ) + Report.result(Tests.parameter_removed, result) + editor.AssetEditorWidgetRequestsBus(bus.Broadcast, "SaveAssetAs", file_path) + + # 1) Open Asset Editor + general.idle_enable(True) + # Initially close the Asset Editor and then reopen to ensure we don't have any existing assets open + general.close_pane("Asset Editor") + general.open_pane("Asset Editor") + helper.wait_for_condition(lambda: general.is_pane_visible("Asset Editor"), 5.0) + + # 2) Get Asset Editor Qt object + editor_window = pyside_utils.get_editor_main_window() + asset_editor_widget = editor_window.findChild(QtWidgets.QDockWidget, "Asset Editor").findChild( + QtWidgets.QWidget, "AssetEditorWindowClass" + ) + container = asset_editor_widget.findChild(QtWidgets.QWidget, "ContainerForRows") + menu_bar = asset_editor_widget.findChild(QtWidgets.QMenuBar) + + # 3) Create new Script Event Asset + create_script_event(asset_editor_widget, FILE_PATH) + + # 4) Add Parameter to Event + create_parameter(FILE_PATH) + + # 5) Remove Parameter from Event + remove_parameter(FILE_PATH) + + +if __name__ == "__main__": + import ImportPathHelper as imports + + imports.init() + from editor_python_test_tools.utils import Report + + Report.start_test(ScriptEvent_AddRemoveParameter_ActionsSuccessful) diff --git a/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py b/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py index 85d0b4523f..46d07250bc 100755 --- a/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py +++ b/AutomatedTesting/Gem/PythonTests/scripting/TestSuite_Periodic.py @@ -190,6 +190,18 @@ class TestAutomation(TestAutomationBase): from . import Node_HappyPath_DuplicateNode as test_module self._run_test(request, workspace, editor, test_module) + def test_ScriptEvent_AddRemoveParameter_ActionsSuccessful(self, request, workspace, editor, launcher_platform): + def teardown(): + file_system.delete( + [os.path.join(workspace.paths.project(), "ScriptCanvas", "test_file.scriptevent")], True, True + ) + request.addfinalizer(teardown) + file_system.delete( + [os.path.join(workspace.paths.project(), "ScriptCanvas", "test_file.scriptevent")], True, True + ) + from . import ScriptEvent_AddRemoveParameter_ActionsSuccessful as test_module + self._run_test(request, workspace, editor, test_module) + # NOTE: We had to use hydra_test_utils.py, as TestAutomationBase run_test method # fails because of pyside_utils import @pytest.mark.SUITE_periodic From 8a119f2b18bc643f4841182380eb7dd443796a2a Mon Sep 17 00:00:00 2001 From: zsolleci Date: Wed, 26 May 2021 12:33:46 -0500 Subject: [PATCH 07/53] fixed error in test steps description --- .../ScriptEvent_AddRemoveParameter_ActionsSuccessful.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py b/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py index a587354985..638c69e8f3 100644 --- a/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py +++ b/AutomatedTesting/Gem/PythonTests/scripting/ScriptEvent_AddRemoveParameter_ActionsSuccessful.py @@ -33,9 +33,7 @@ def ScriptEvent_AddRemoveParameter_ActionsSuccessful(): 2) Get Asset Editor Qt object 3) Create new Script Event Asset 4) Add Parameter to Event - 5) Verify Parameter exists - 6) Remove Parameter from Event - 7) Verify Parameter has been removed + 5) Remove Parameter from Event Note: - This test file must be called from the Open 3D Engine Editor command terminal From 05a0e063a22edab79891fc6c7b34e61675334121 Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Wed, 26 May 2021 10:38:04 -0700 Subject: [PATCH 08/53] Also making Auton->Auth Getters requiring controller --- .../Code/Source/AutoGen/AutoComponent_Header.jinja | 2 -- .../Code/Source/AutoGen/AutoComponent_Source.jinja | 8 +++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja index aeab2e88e1..96c5433ce0 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja @@ -440,7 +440,6 @@ namespace {{ Component.attrib['Namespace'] }} {% endif %} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Server', false)|indent(8) -}} - {{ DeclareNetworkPropertyGetters(Component, 'Autonomous', 'Authority', false)|indent(8) -}} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Client', false)|indent(8) -}} {{ DeclareArchetypePropertyGetters(Component)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', false)|indent(8) -}} @@ -465,7 +464,6 @@ namespace {{ Component.attrib['Namespace'] }} //! @} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Server', true)|indent(8) -}} - {{ DeclareNetworkPropertyGetters(Component, 'Autonomous', 'Authority', true)|indent(8) -}} {{ DeclareNetworkPropertyGetters(Component, 'Authority', 'Client', true)|indent(8) -}} {{ DeclareRpcInvocations(Component, 'Server', 'Authority', true)|indent(8) -}} {{ AutoComponentMacros.DeclareRpcHandlers(Component, 'Authority', 'Client', false)|indent(8) -}} diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja index 3ca4d854bb..b782d7e583 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja @@ -767,15 +767,15 @@ enum class NetworkProperties return {{ Property.attrib['Type'] }}(); } - {{ ClassName }}* networkComponent = entity->FindComponent<{{ ClassName }}>(); + {{ ClassName }}* networkComponent = entity->FindComponent<{{ ClassName }}>(); if (!networkComponent) { AZ_Warning("Network Property", false, "{{ ClassName }} Get{{ UpperFirst(Property.attrib['Name']) }} failed. Entity '%s' (id: %s) is missing {{ ClassName }}, be sure to add {{ ClassName }} to this entity.", entity->GetName().c_str(), id.ToString().c_str()) return {{ Property.attrib['Type'] }}(); } -{% if ReplicateTo == 'Autonomous' %} +{% if (ReplicateTo == 'Autonomous') or (ReplicateFrom == 'Autonomous' and ReplicateTo == 'Authority') %} - // {{ UpperFirst(Property.attrib['Name']) }} is replicated to Automonous; we must go through the controller in order to get this property + // {{ UpperFirst(Property.attrib['Name']) }} is only sent and received between contoller objects (ie Authority, Autonomous); we must go through the controller in order to get this property {{ ClassName }}Controller* controller = static_cast<{{ ClassName }}Controller*>(networkComponent->GetController()); if (!controller) { @@ -1472,10 +1472,8 @@ namespace {{ Component.attrib['Namespace'] }} } {{ DefineNetworkPropertyGets(Component, 'Authority', 'Server', false, ComponentBaseName)|indent(4) -}} -{{ DefineNetworkPropertyGets(Component, 'Autonomous', 'Authority', false, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Client', false, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Server', true, ComponentBaseName)|indent(4) -}} -{{ DefineNetworkPropertyGets(Component, 'Autonomous', 'Authority', true, ComponentBaseName)|indent(4) -}} {{ DefineNetworkPropertyGets(Component, 'Authority', 'Client', true, ComponentBaseName)|indent(4) }} {{ DefineArchetypePropertyGets(Component, ClassType, ComponentBaseName)|indent(4) -}} {{ DefineRpcInvocations(Component, ComponentBaseName, 'Server', 'Authority', false)|indent(4) -}} From 37b53c06800f5eb4dcb4a86de53abb2bea51645b Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Tue, 25 May 2021 09:13:11 -0700 Subject: [PATCH 09/53] Spawnables can no longer be moved. Spawnables had support for moving, but as the base class AZ::Data::AssetData doesn't support moving this was causing subtle issues. Moving spawnables wasn't used so it was removed. --- .../AzFramework/Spawnable/Spawnable.cpp | 16 ---------------- .../AzFramework/Spawnable/Spawnable.h | 4 ++-- .../Prefab/Spawnable/SpawnableUtils.cpp | 11 ----------- .../Prefab/Spawnable/SpawnableUtils.h | 1 - 4 files changed, 2 insertions(+), 30 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 7ab2d48814..46b3dfe87c 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -21,22 +21,6 @@ namespace AzFramework { } - Spawnable::Spawnable(Spawnable&& other) - : m_entities(AZStd::move(other.m_entities)) - { - } - - - Spawnable& Spawnable::operator=(Spawnable&& other) - { - if (this != &other) - { - m_entities = AZStd::move(other.m_entities); - } - - return *this; - } - const Spawnable::EntityList& Spawnable::GetEntities() const { return m_entities; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 79cea647e4..677f0326cf 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -41,11 +41,11 @@ namespace AzFramework Spawnable() = default; explicit Spawnable(const AZ::Data::AssetId& id, AssetStatus status = AssetStatus::NotLoaded); Spawnable(const Spawnable& rhs) = delete; - Spawnable(Spawnable&& other); + Spawnable(Spawnable&& other) = delete; ~Spawnable() override = default; Spawnable& operator=(const Spawnable& rhs) = delete; - Spawnable& operator=(Spawnable&& other); + Spawnable& operator=(Spawnable&& other) = delete; const EntityList& GetEntities() const; EntityList& GetEntities(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp index 716c3098d9..3c91f99b05 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.cpp @@ -24,17 +24,6 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { - - AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom) - { - AzFramework::Spawnable spawnable; - AZStd::vector> referencedAssets; - [[maybe_unused]] bool result = CreateSpawnable(spawnable, prefabDom, referencedAssets); - AZ_Assert(result, - "Failed to Load Prefab Instance from given Prefab DOM while Spawnable creation."); - return spawnable; - } - bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom) { AZStd::vector> referencedAssets; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h index 3b5ea488cb..cdf07346d0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h @@ -17,7 +17,6 @@ namespace AzToolsFramework::Prefab::SpawnableUtils { - AzFramework::Spawnable CreateSpawnable(const PrefabDom& prefabDom); bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom); bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector>& referencedAssets); From 76827ff95eab5bfbba5e2b763fb46ef4156db9aa Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Wed, 26 May 2021 09:23:53 -0700 Subject: [PATCH 10/53] Added support for a priority lane for entity spawning It's now possible to have high and normal priority calls on the spawnable entities manager. This allows for events like (de)spawning and retrieving information to be executed before already queued requests, though requests cannot be reordered on the same ticket. High priority calls are executed twice per frame, while normal priority calls are called only once. --- .../Spawnable/SpawnableEntitiesContainer.cpp | 18 ++- .../Spawnable/SpawnableEntitiesInterface.h | 58 ++++++-- .../Spawnable/SpawnableEntitiesManager.cpp | 127 +++++++++--------- .../Spawnable/SpawnableEntitiesManager.h | 64 ++++++--- .../Spawnable/SpawnableSystemComponent.cpp | 18 ++- .../Spawnable/SpawnableSystemComponent.h | 8 ++ .../SpawnableEntitiesManagerTests.cpp | 84 ++++++++++-- 7 files changed, 266 insertions(+), 111 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp index 673701cac4..e9a78dccde 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp @@ -38,19 +38,20 @@ namespace AzFramework void SpawnableEntitiesContainer::SpawnAllEntities() { AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket); + SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); } void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector entityIndices) { AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->SpawnEntities(m_threadData->m_spawnedEntitiesTicket, AZStd::move(entityIndices)); + SpawnableEntitiesInterface::Get()->SpawnEntities( + m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(entityIndices)); } void SpawnableEntitiesContainer::DespawnAllEntities() { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket); + SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); } void SpawnableEntitiesContainer::Reset(AZ::Data::Asset spawnable) @@ -66,7 +67,9 @@ namespace AzFramework m_monitor.Disconnect(); m_monitor.m_threadData.reset(); - SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, + SpawnableEntitiesInterface::Get()->Barrier( + m_threadData->m_spawnedEntitiesTicket, + SpawnablePriorty_Default, [threadData = m_threadData](EntitySpawnTicket&) mutable { threadData.reset(); @@ -83,7 +86,9 @@ namespace AzFramework void SpawnableEntitiesContainer::Alert(AlertCallback callback) { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, + SpawnableEntitiesInterface::Get()->Barrier( + m_threadData->m_spawnedEntitiesTicket, + SpawnablePriorty_Default, [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket&) { callback(generation); @@ -110,6 +115,7 @@ namespace AzFramework AZ_Assert(m_threadData, "SpawnableEntitiesContainer is monitoring a spawnable, but doesn't have the associated data."); AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str()); - SpawnableEntitiesInterface::Get()->ReloadSpawnable(m_threadData->m_spawnedEntitiesTicket, AZStd::move(replacementAsset)); + SpawnableEntitiesInterface::Get()->ReloadSpawnable( + m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(replacementAsset)); } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 69bca8e111..97d06e1f37 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -24,6 +25,14 @@ namespace AZ namespace AzFramework { + AZ_TYPE_SAFE_INTEGRAL(SpawnablePriority, uint8_t); + + inline static constexpr SpawnablePriority SpawnablePriorty_Highest { 0 }; + inline static constexpr SpawnablePriority SpawnablePriorty_High { 32 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Default { 128 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Low { 192 }; + inline static constexpr SpawnablePriority SpawnablePriorty_Lowest { 255 }; + class SpawnableEntityContainerView { public: @@ -124,10 +133,10 @@ namespace AzFramework SpawnableIndexEntityIterator m_end; }; - //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that be used as a template. A ticket can - //! be reused for multiple calls on the same spawnable and is safe to use by multiple threads at the same time. Entities created + //! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can + //! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created //! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created - //! by a call so spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a + //! by a call to spawn entities. The life cycle of the spawned entities is tied to the ticket and all entities spawned using a //! ticket will be despawned when it's deleted. class EntitySpawnTicket { @@ -159,10 +168,19 @@ namespace AzFramework using BarrierCallback = AZStd::function; //! Interface definition to (de)spawn entities from a spawnable into the game world. + //! //! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be //! issued from threads other than the one that issued the call, including the main thread. + //! //! Calls on the same ticket are guaranteed to be executed in the order they are issued. Note that when issuing requests from //! multiple threads on the same ticket the order in which the requests are assigned to the ticket is not guaranteed. + //! + //! Most calls have a priority where values closer to 0 mean higher priority than values closer to 255. The implementation of this + //! interface may choose to use priority lanes which doesn't guarantee that higher priority requests happen before lower priority + //! requests if they don't pass the priority lane threshold. Priority lanes and their thresholds are implementation specific and may + //! differ between platforms. Note that if a call happened on a ticket with lower priority followed by a one with a higher priority + //! the first lower priority call will still needs to complete before the second higher priority call can be executed and the priority + //! of the first call will not be updated. class SpawnableEntitiesDefinition { public: @@ -173,40 +191,48 @@ namespace AzFramework virtual ~SpawnableEntitiesDefinition() = default; //! Spawn instances of all entities in the spawnable. - //! @param spawnable The Spawnable asset that will be used to create entity instances from. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param priority The priority at which this call will be executed. //! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from //! a different thread than the one that made the function call. The returned list of entities contains all the newly //! created entities. - virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, + virtual void SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0; //! Spawn instances of some entities in the spawnable. //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param priority The priority at which this call will be executed. //! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from. //! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from //! a different thread than the one that made this function call. The returned list of entities contains all the newly //! created entities. - virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, + virtual void SpawnEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0; //! Removes all entities in the provided list from the environment. //! @param ticket The ticket previously used to spawn entities with. + //! @param priority The priority at which this call will be executed. //! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from //! a different thread than the one that made this function call. - virtual void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) = 0; + virtual void DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) = 0; //! Removes all entities in the provided list from the environment and reconstructs the entities from the provided spawnable. - //! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them. + //! @param ticket Holds the information on the entities to reload. + //! @param priority The priority at which this call will be executed. //! @param spawnable The spawnable that will replace the existing spawnable. Both need to have the same asset id. //! @param completionCallback Optional callback that's called when the entities have been reloaded. This can be called from //! a different thread than the one that made this function call. The returned list of entities contains all the replacement //! entities. - virtual void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + virtual void ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback = {}) = 0; //! List all entities that are spawned using this ticket. //! @param ticket Only the entities associated with this ticket will be listed. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to list the entities on. - virtual void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) = 0; + virtual void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) = 0; //! List all entities that are spawned using this ticket with their spawnable index. //! Spawnables contain a flat list of entities, which are used as templates to spawn entities from. For every spawned entity //! the index of the entity in the spawnable that was used as a template is stored. This version of ListEntities will return @@ -214,17 +240,23 @@ namespace AzFramework //! the same index may appear multiple times as there are no restriction on how many instance of a specific entity can be //! created. //! @param ticket Only the entities associated with this ticket will be listed. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to list the entities and indices on. - virtual void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) = 0; + virtual void ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) = 0; //! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the //! caller through the callback. After this call the ticket will have no entities associated with it. The caller of //! this function will need to manage the entities after this call. //! @param ticket Only the entities associated with this ticket will be released. + //! @param priority The priority at which this call will be executed. //! @param listCallback Required callback that will be called to transfer the entities through. - virtual void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) = 0; + virtual void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) = 0; //! Blocks until all operations made on the provided ticket before the barrier call have completed. - virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) = 0; + //! @param ticket The ticket to monitor. + //! @param priority The priority at which this call will be executed. + //! @param completionCallback Required callback that will be called as soon as the barrier has been reached. + virtual void Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) = 0; //! Register a handler for OnSpawned events. virtual void AddOnSpawnedHandler(AZ::Event>::Handler& handler) = 0; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 7e20f7b265..959d2ab64f 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -22,7 +22,19 @@ namespace AzFramework { - void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback, + template + void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request) + { + Queue& queue = priority <= HighPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue; + { + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + request.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; + queue.m_pendingRequest.push(AZStd::move(request)); + } + } + + void SpawnableEntitiesManager::SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized."); @@ -31,15 +43,11 @@ namespace AzFramework queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } void SpawnableEntitiesManager::SpawnEntities( - EntitySpawnTicket& ticket, AZStd::vector entityIndices, + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized."); @@ -49,28 +57,22 @@ namespace AzFramework queueEntry.m_entityIndices = AZStd::move(entityIndices); queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback) + void SpawnableEntitiesManager::DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized."); DespawnAllEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + void SpawnableEntitiesManager::ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback) { AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized."); @@ -79,14 +81,10 @@ namespace AzFramework queueEntry.m_ticket = &ticket; queueEntry.m_spawnable = AZStd::move(spawnable); queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) + void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) { AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); @@ -94,14 +92,11 @@ namespace AzFramework ListEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) + void SpawnableEntitiesManager::ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) { AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); @@ -109,14 +104,10 @@ namespace AzFramework ListIndicesEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) + void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) { AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized."); @@ -124,14 +115,10 @@ namespace AzFramework ClaimEntitiesCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_listCallback = AZStd::move(listCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } - void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback) + void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) { AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use."); AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized."); @@ -139,11 +126,7 @@ namespace AzFramework BarrierCommand queueEntry; queueEntry.m_ticket = &ticket; queueEntry.m_completionCallback = AZStd::move(completionCallback); - { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - queueEntry.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); - } + QueueRequest(ticket, priority, AZStd::move(queueEntry)); } void SpawnableEntitiesManager::AddOnSpawnedHandler(AZ::Event>::Handler& handler) @@ -156,34 +139,54 @@ namespace AzFramework handler.Connect(m_onDespawnedEvent); } - auto SpawnableEntitiesManager::ProcessQueue() -> CommandQueueStatus + auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus + { + CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft; + if ((priority & CommandQueuePriority::High) == CommandQueuePriority::High) + { + if (ProcessQueue(m_highPriorityQueue) == CommandQueueStatus::HasCommandsLeft) + { + result = CommandQueueStatus::HasCommandsLeft; + } + } + if ((priority & CommandQueuePriority::Regular) == CommandQueuePriority::Regular) + { + if (ProcessQueue(m_regularPriorityQueue) == CommandQueueStatus::HasCommandsLeft) + { + result = CommandQueueStatus::HasCommandsLeft; + } + } + return result; + } + + auto SpawnableEntitiesManager::ProcessQueue(Queue& queue) -> CommandQueueStatus { AZStd::queue pendingRequestQueue; { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - m_pendingRequestQueue.swap(pendingRequestQueue); + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + queue.m_pendingRequest.swap(pendingRequestQueue); } - if (!pendingRequestQueue.empty() || !m_delayedQueue.empty()) + if (!pendingRequestQueue.empty() || !queue.m_delayed.empty()) { AZ::SerializeContext* serializeContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext); AZ_Assert(serializeContext, "Failed to retrieve serialization context."); // Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete. - size_t delayedSize = m_delayedQueue.size(); + size_t delayedSize = queue.m_delayed.size(); for (size_t i = 0; i < delayedSize; ++i) { - Requests& request = m_delayedQueue.front(); + Requests& request = queue.m_delayed.front(); bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool { return ProcessRequest(args, *serializeContext); }, request); if (!result) { - m_delayedQueue.emplace_back(AZStd::move(request)); + queue.m_delayed.emplace_back(AZStd::move(request)); } - m_delayedQueue.pop_front(); + queue.m_delayed.pop_front(); } do @@ -197,7 +200,7 @@ namespace AzFramework }, request); if (!result) { - m_delayedQueue.emplace_back(AZStd::move(request)); + queue.m_delayed.emplace_back(AZStd::move(request)); } pendingRequestQueue.pop(); } @@ -205,13 +208,13 @@ namespace AzFramework // Spawning entities can result in more entities being queued to spawn. Repeat spawning until the queue is // empty to avoid a chain of entity spawning getting dragged out over multiple frames. { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); - m_pendingRequestQueue.swap(pendingRequestQueue); + AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); + queue.m_pendingRequest.swap(pendingRequestQueue); } } while (!pendingRequestQueue.empty()); } - return m_delayedQueue.empty() ? CommandQueueStatus::NoCommandLeft : CommandQueueStatus::HasCommandsLeft; + return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft; } void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset&& spawnable) @@ -226,9 +229,9 @@ namespace AzFramework DestroyTicketCommand queueEntry; queueEntry.m_ticket = reinterpret_cast(ticket); { - AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex); + AZStd::scoped_lock queueLock(m_regularPriorityQueue.m_pendingRequestMutex); queueEntry.m_ticketId = reinterpret_cast(ticket)->m_nextTicketId++; - m_pendingRequestQueue.push(AZStd::move(queueEntry)); + m_regularPriorityQueue.m_pendingRequest.push(AZStd::move(queueEntry)); } } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 3481ab180a..137376ac7b 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -29,8 +29,6 @@ namespace AZ namespace AzFramework { - using EntityIdMap = AZStd::unordered_map; - class SpawnableEntitiesManager : public SpawnableEntitiesInterface::Registrar { @@ -38,31 +36,48 @@ namespace AzFramework AZ_RTTI(AzFramework::SpawnableEntitiesManager, "{6E14333F-128C-464C-94CA-A63B05A5E51C}"); AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0); + using EntityIdMap = AZStd::unordered_map; + enum class CommandQueueStatus : bool { HasCommandsLeft, - NoCommandLeft + NoCommandsLeft }; + enum class CommandQueuePriority + { + High = 1 << 0, + Regular = 1 << 1 + }; + + static constexpr SpawnablePriority HighPriorityThreshold = SpawnablePriority { 64 }; + ~SpawnableEntitiesManager() override = default; // // The following functions are thread safe // - void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; - void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector entityIndices, EntityPreInsertionCallback preInsertionCallback = {}, + void SpawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override; - void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override; + void SpawnEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector entityIndices, + EntityPreInsertionCallback preInsertionCallback = {}, + EntitySpawnCallback completionCallback = {}) override; + void DespawnAllEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) override; - void ReloadSpawnable(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, + void ReloadSpawnable( + EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset spawnable, ReloadSpawnableCallback completionCallback = {}) override; - void ListEntities(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback) override; - void ListIndicesAndEntities(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback) override; - void ClaimEntities(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback) override; + void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) override; + void ListIndicesAndEntities( + EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) override; + void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) override; - void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback) override; + void Barrier(EntitySpawnTicket& spawnInfo, SpawnablePriority priority, BarrierCallback completionCallback) override; void AddOnSpawnedHandler(AZ::Event>::Handler& handler) override; void AddOnDespawnedHandler(AZ::Event>::Handler& handler) override; @@ -71,13 +86,9 @@ namespace AzFramework // The following function is thread safe but intended to be run from the main thread. // - CommandQueueStatus ProcessQueue(); + CommandQueueStatus ProcessQueue(CommandQueuePriority priority); protected: - void* CreateTicket(AZ::Data::Asset&& spawnable) override; - void DestroyTicket(void* ticket) override; - - private: struct Ticket { AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0); @@ -153,6 +164,20 @@ namespace AzFramework SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand, ListEntitiesCommand, ListIndicesEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>; + struct Queue + { + AZStd::deque m_delayed; //!< Requests that were processed before, but couldn't be completed. + AZStd::queue m_pendingRequest; //!< Requests waiting to be processed for the first time. + AZStd::mutex m_pendingRequestMutex; + }; + + template + void QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request); + void* CreateTicket(AZ::Data::Asset&& spawnable) override; + void DestroyTicket(void* ticket) override; + + CommandQueueStatus ProcessQueue(Queue& queue); + AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext); @@ -174,11 +199,12 @@ namespace AzFramework [[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs); [[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const Ticket* rhs); - AZStd::deque m_delayedQueue; //!< Requests that were processed before, but couldn't be completed. - AZStd::queue m_pendingRequestQueue; - AZStd::mutex m_pendingRequestQueueMutex; + Queue m_highPriorityQueue; + Queue m_regularPriorityQueue; AZ::Event> m_onSpawnedEvent; AZ::Event> m_onDespawnedEvent; }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(AzFramework::SpawnableEntitiesManager::CommandQueuePriority); } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp index 262f006f15..32cc61914e 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp @@ -48,10 +48,23 @@ namespace AzFramework void SpawnableSystemComponent::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { - m_entitiesManager.ProcessQueue(); + m_entitiesManager.ProcessQueue( + SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular); RootSpawnableNotificationBus::ExecuteQueuedEvents(); } + int SpawnableSystemComponent::GetTickOrder() + { + return AZ::ComponentTickBus::TICK_GAME; + } + + void SpawnableSystemComponent::OnSystemTick() + { + // Handle only high priority spawning events such as those created from network. These need to happen even if the server + // doesn't have focus to avoid + m_entitiesManager.ProcessQueue(SpawnableEntitiesManager::CommandQueuePriority::High); + } + void SpawnableSystemComponent::OnCatalogLoaded([[maybe_unused]] const char* catalogFile) { if (!m_catalogAvailable) @@ -168,7 +181,8 @@ namespace AzFramework SpawnableEntitiesManager::CommandQueueStatus queueStatus; do { - queueStatus = m_entitiesManager.ProcessQueue(); + queueStatus = m_entitiesManager.ProcessQueue( + SpawnableEntitiesManager::CommandQueuePriority::High | SpawnableEntitiesManager::CommandQueuePriority::Regular); } while (queueStatus == SpawnableEntitiesManager::CommandQueueStatus::HasCommandsLeft); } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h index b29fbea5e4..1549b86385 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.h @@ -28,6 +28,7 @@ namespace AzFramework class SpawnableSystemComponent : public AZ::Component , public AZ::TickBus::Handler + , public AZ::SystemTickBus::Handler , public AssetCatalogEventBus::Handler , public RootSpawnableInterface::Registrar , public RootSpawnableNotificationBus::Handler @@ -58,6 +59,13 @@ namespace AzFramework // void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + int GetTickOrder() override; + + // + // SystemTickBus + // + + void OnSystemTick() override; // // AssetCatalogEventBus diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 1fa50fff52..191b14c31d 100644 --- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -55,7 +55,11 @@ namespace UnitTest delete m_ticket; m_ticket = nullptr; // One more tick on the spawnable entities manager in order to delete the ticket fully. - m_manager->ProcessQueue(); + while (m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular) != + AzFramework::SpawnableEntitiesManager::CommandQueueStatus::NoCommandsLeft) + ; delete m_spawnableAsset; m_spawnableAsset = nullptr; @@ -96,8 +100,8 @@ namespace UnitTest { spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket, {}, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(NumEntities, spawnedEntitiesCount); } @@ -119,9 +123,9 @@ namespace UnitTest spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListEntities(*m_ticket, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); + m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); EXPECT_EQ(NumEntities, spawnedEntitiesCount); @@ -148,11 +152,73 @@ namespace UnitTest } }; - m_manager->SpawnAllEntities(*m_ticket); - m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback)); - m_manager->ProcessQueue(); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); + m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + + TEST_F(SpawnableEntitiesManagerTest, Priority_HighBeforeDefault_HigherPriorityCallHappensBeforeDefaultPriorityEvenWhenQueuedLater) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + AzFramework::EntitySpawnTicket highPriorityTicket(*m_spawnableAsset); + + size_t callCounter = 1; + size_t highPriorityCallId = 0; + size_t defaultPriorityCallId = 0; + auto highCallback = [&callCounter, &highPriorityCallId] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + highPriorityCallId = callCounter++; + }; + auto defaultCallback = [&callCounter, &defaultPriorityCallId] + (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + defaultPriorityCallId = callCounter++; + }; + + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_LT(highPriorityCallId, defaultPriorityCallId); + } + + TEST_F(SpawnableEntitiesManagerTest, Priority_SameTicket_DefaultPriorityCallHappensBeforeHighPriority) + { + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + size_t callCounter = 1; + size_t highPriorityCallId = 0; + size_t defaultPriorityCallId = 0; + auto highCallback = + [&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + highPriorityCallId = callCounter++; + }; + auto defaultCallback = + [&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + { + defaultPriorityCallId = callCounter++; + }; + + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + // Run a second time as the high priority task will be pending at this point. + m_manager->ProcessQueue( + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | + AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_LT(defaultPriorityCallId, highPriorityCallId); + } } // namespace UnitTest From e0948a26bc1cd169de8ae5a2f82bc8d69794aa1e Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Wed, 26 May 2021 14:26:16 -0700 Subject: [PATCH 11/53] Fixed early ticket delete crash This commit fixes a crash that could happen when a spawnable ticket was deleted before all requests in the queue had completed. Because of this crash the requests now only hold on to the payload of the ticket but not the ticket itself. As a side effect, callbacks can no longer provide the ticket itself so instead a unique id for the ticket is returned. --- .../Spawnable/SpawnableEntitiesContainer.cpp | 4 +- .../Spawnable/SpawnableEntitiesInterface.cpp | 13 +- .../Spawnable/SpawnableEntitiesInterface.h | 22 +-- .../Spawnable/SpawnableEntitiesManager.cpp | 101 ++++++------ .../Spawnable/SpawnableEntitiesManager.h | 48 +++--- .../SpawnableEntitiesManagerTests.cpp | 150 ++++++++++++++++-- 6 files changed, 242 insertions(+), 96 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp index e9a78dccde..9b06eb1f20 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp @@ -70,7 +70,7 @@ namespace AzFramework SpawnableEntitiesInterface::Get()->Barrier( m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, - [threadData = m_threadData](EntitySpawnTicket&) mutable + [threadData = m_threadData](EntitySpawnTicket::Id) mutable { threadData.reset(); }); @@ -89,7 +89,7 @@ namespace AzFramework SpawnableEntitiesInterface::Get()->Barrier( m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, - [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket&) + [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id) { callback(generation); }); diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp index 97169ebeb3..26a10933b5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp @@ -239,7 +239,9 @@ namespace AzFramework { auto manager = SpawnableEntitiesInterface::Get(); AZ_Assert(manager, "Attempting to create an entity spawn ticket while the SpawnableEntitiesInterface has no implementation."); - m_payload = manager->CreateTicket(AZStd::move(spawnable)); + AZStd::pair result = manager->CreateTicket(AZStd::move(spawnable)); + m_id = result.first; + m_payload = result.second; } EntitySpawnTicket::~EntitySpawnTicket() @@ -250,6 +252,7 @@ namespace AzFramework AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation."); manager->DestroyTicket(m_payload); m_payload = nullptr; + m_id = 0; } } @@ -263,12 +266,20 @@ namespace AzFramework AZ_Assert(manager, "Attempting to destroy an entity spawn ticket while the SpawnableEntitiesInterface has no implementation."); manager->DestroyTicket(m_payload); } + m_id = rhs.m_id; + rhs.m_id = 0; + m_payload = rhs.m_payload; rhs.m_payload = nullptr; } return *this; } + uint64_t EntitySpawnTicket::GetId() const + { + return m_id; + } + bool EntitySpawnTicket::IsValid() const { return m_payload != nullptr; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index 97d06e1f37..b40136def7 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -143,6 +143,8 @@ namespace AzFramework public: friend class SpawnableEntitiesDefinition; + using Id = uint64_t; + EntitySpawnTicket() = default; EntitySpawnTicket(const EntitySpawnTicket&) = delete; EntitySpawnTicket(EntitySpawnTicket&& rhs); @@ -152,20 +154,22 @@ namespace AzFramework EntitySpawnTicket& operator=(const EntitySpawnTicket&) = delete; EntitySpawnTicket& operator=(EntitySpawnTicket&& rhs); + uint64_t GetId() const; bool IsValid() const; private: void* m_payload{ nullptr }; + Id m_id { 0 }; //!< An id that uniquely identifies a ticket. }; - using EntitySpawnCallback = AZStd::function; - using EntityPreInsertionCallback = AZStd::function; - using EntityDespawnCallback = AZStd::function; - using ReloadSpawnableCallback = AZStd::function; - using ListEntitiesCallback = AZStd::function; - using ListIndicesEntitiesCallback = AZStd::function; - using ClaimEntitiesCallback = AZStd::function; - using BarrierCallback = AZStd::function; + using EntitySpawnCallback = AZStd::function; + using EntityPreInsertionCallback = AZStd::function; + using EntityDespawnCallback = AZStd::function; + using ReloadSpawnableCallback = AZStd::function; + using ListEntitiesCallback = AZStd::function; + using ListIndicesEntitiesCallback = AZStd::function; + using ClaimEntitiesCallback = AZStd::function; + using BarrierCallback = AZStd::function; //! Interface definition to (de)spawn entities from a spawnable into the game world. //! @@ -265,7 +269,7 @@ namespace AzFramework virtual void AddOnDespawnedHandler(AZ::Event>::Handler& handler) = 0; protected: - [[nodiscard]] virtual void* CreateTicket(AZ::Data::Asset&& spawnable) = 0; + [[nodiscard]] virtual AZStd::pair CreateTicket(AZ::Data::Asset&& spawnable) = 0; virtual void DestroyTicket(void* ticket) = 0; template diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 959d2ab64f..caf1112e9b 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -25,10 +25,11 @@ namespace AzFramework template void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request) { + request.m_ticket = &GetTicketPayload(ticket); Queue& queue = priority <= HighPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue; { AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); - request.m_ticketId = GetTicketPayload(ticket).m_nextTicketId++; + request.m_requestId = GetTicketPayload(ticket).m_nextRequestId++; queue.m_pendingRequest.push(AZStd::move(request)); } } @@ -40,7 +41,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized."); SpawnAllEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); @@ -53,7 +54,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized."); SpawnEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_entityIndices = AZStd::move(entityIndices); queueEntry.m_completionCallback = AZStd::move(completionCallback); queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback); @@ -66,7 +67,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized."); DespawnAllEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_completionCallback = AZStd::move(completionCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); } @@ -78,7 +79,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized."); ReloadSpawnableCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_spawnable = AZStd::move(spawnable); queueEntry.m_completionCallback = AZStd::move(completionCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); @@ -90,7 +91,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); ListEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_listCallback = AZStd::move(listCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); } @@ -102,7 +103,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized."); ListIndicesEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_listCallback = AZStd::move(listCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); } @@ -113,7 +114,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized."); ClaimEntitiesCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_listCallback = AZStd::move(listCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); } @@ -124,7 +125,7 @@ namespace AzFramework AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized."); BarrierCommand queueEntry; - queueEntry.m_ticket = &ticket; + queueEntry.m_ticketId = ticket.GetId(); queueEntry.m_completionCallback = AZStd::move(completionCallback); QueueRequest(ticket, priority, AZStd::move(queueEntry)); } @@ -217,11 +218,13 @@ namespace AzFramework return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft; } - void* SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset&& spawnable) + AZStd::pair SpawnableEntitiesManager::CreateTicket(AZ::Data::Asset&& spawnable) { + static AZStd::atomic_uint64_t idCounter { 1 }; + auto result = aznew Ticket(); result->m_spawnable = AZStd::move(spawnable); - return result; + return AZStd::make_pair(idCounter++, result); } void SpawnableEntitiesManager::DestroyTicket(void* ticket) @@ -230,7 +233,7 @@ namespace AzFramework queueEntry.m_ticket = reinterpret_cast(ticket); { AZStd::scoped_lock queueLock(m_regularPriorityQueue.m_pendingRequestMutex); - queueEntry.m_ticketId = reinterpret_cast(ticket)->m_nextTicketId++; + queueEntry.m_requestId = reinterpret_cast(ticket)->m_nextRequestId++; m_regularPriorityQueue.m_pendingRequest.push(AZStd::move(queueEntry)); } } @@ -254,8 +257,8 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -303,7 +306,7 @@ namespace AzFramework // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. if (request.m_preInsertionCallback) { - request.m_preInsertionCallback(*request.m_ticket, SpawnableEntityContainerView( + request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView( ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } @@ -317,13 +320,13 @@ namespace AzFramework // Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context. if (request.m_completionCallback) { - request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView( + request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView( ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } m_onSpawnedEvent.Signal(ticket.m_spawnable); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -334,8 +337,8 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -370,9 +373,7 @@ namespace AzFramework // Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context. if (request.m_preInsertionCallback) { - request.m_preInsertionCallback( - *request.m_ticket, - SpawnableEntityContainerView( + request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView( ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } @@ -385,13 +386,13 @@ namespace AzFramework if (request.m_completionCallback) { - request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView( + request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView( ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end())); } m_onSpawnedEvent.Signal(ticket.m_spawnable); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -403,8 +404,8 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (request.m_requestId == ticket.m_currentRequestId) { for (AZ::Entity* entity : ticket.m_spawnedEntities) { @@ -420,12 +421,12 @@ namespace AzFramework if (request.m_completionCallback) { - request.m_completionCallback(*request.m_ticket); + request.m_completionCallback(request.m_ticketId); } m_onDespawnedEvent.Signal(ticket.m_spawnable); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -436,11 +437,11 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); + Ticket& ticket = *request.m_ticket; AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(), "Spawnable is being reloaded, but the provided spawnable has a different asset id. " "This will likely result in unexpected entities being created."); - if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId) + if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { // Delete the original entities. for (AZ::Entity* entity : ticket.m_spawnedEntities) @@ -496,11 +497,11 @@ namespace AzFramework if (request.m_completionCallback) { - request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView( + request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end())); } - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; m_onSpawnedEvent.Signal(ticket.m_spawnable); @@ -514,12 +515,12 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (request.m_requestId == ticket.m_currentRequestId) { - request.m_listCallback(*request.m_ticket, SpawnableConstEntityContainerView( + request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end())); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -530,17 +531,15 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (request.m_requestId == ticket.m_currentRequestId) { AZ_Assert( ticket.m_spawnedEntities.size() == ticket.m_spawnedEntityIndices.size(), "Entities and indices on spawnable ticket have gone out of sync."); - request.m_listCallback( - *request.m_ticket, - SpawnableConstIndexEntityContainerView( + request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size())); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -551,16 +550,16 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (request.m_requestId == ticket.m_currentRequestId) { - request.m_listCallback(*request.m_ticket, SpawnableEntityContainerView( + request.m_listCallback(request.m_ticketId, SpawnableEntityContainerView( ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end())); ticket.m_spawnedEntities.clear(); ticket.m_spawnedEntityIndices.clear(); - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -571,15 +570,15 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - Ticket& ticket = GetTicketPayload(*request.m_ticket); - if (request.m_ticketId == ticket.m_currentTicketId) + Ticket& ticket = *request.m_ticket; + if (request.m_requestId == ticket.m_currentRequestId) { if (request.m_completionCallback) { - request.m_completionCallback(*request.m_ticket); + request.m_completionCallback(request.m_ticketId); } - ticket.m_currentTicketId++; + ticket.m_currentRequestId++; return true; } else @@ -590,7 +589,7 @@ namespace AzFramework bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext) { - if (request.m_ticketId == request.m_ticket->m_currentTicketId) + if (request.m_requestId == request.m_ticket->m_currentRequestId) { for (AZ::Entity* entity : request.m_ticket->m_spawnedEntities) { diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 137376ac7b..b98be60145 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -97,8 +97,8 @@ namespace AzFramework AZStd::vector m_spawnedEntities; AZStd::vector m_spawnedEntityIndices; AZ::Data::Asset m_spawnable; - uint32_t m_nextTicketId{ 0 }; //!< Next id for this ticket. - uint32_t m_currentTicketId{ 0 }; //!< The id for the command that should be executed. + uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket. + uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed. bool m_loadAll{ true }; }; @@ -106,58 +106,66 @@ namespace AzFramework { EntitySpawnCallback m_completionCallback; EntityPreInsertionCallback m_preInsertionCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct SpawnEntitiesCommand { AZStd::vector m_entityIndices; EntitySpawnCallback m_completionCallback; EntityPreInsertionCallback m_preInsertionCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct DespawnAllEntitiesCommand { EntityDespawnCallback m_completionCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct ReloadSpawnableCommand { AZ::Data::Asset m_spawnable; ReloadSpawnableCallback m_completionCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct ListEntitiesCommand { ListEntitiesCallback m_listCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct ListIndicesEntitiesCommand { ListIndicesEntitiesCallback m_listCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct ClaimEntitiesCommand { ClaimEntitiesCallback m_listCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct BarrierCommand { BarrierCallback m_completionCallback; - EntitySpawnTicket* m_ticket; - uint32_t m_ticketId; + Ticket* m_ticket; + EntitySpawnTicket::Id m_ticketId; + uint32_t m_requestId; }; struct DestroyTicketCommand { Ticket* m_ticket; - uint32_t m_ticketId; + uint32_t m_requestId; }; using Requests = AZStd::variant< @@ -173,7 +181,7 @@ namespace AzFramework template void QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request); - void* CreateTicket(AZ::Data::Asset&& spawnable) override; + AZStd::pair CreateTicket(AZ::Data::Asset&& spawnable) override; void DestroyTicket(void* ticket) override; CommandQueueStatus ProcessQueue(Queue& queue); diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 191b14c31d..320e2ec435 100644 --- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -89,6 +89,10 @@ namespace UnitTest TestApplication* m_application { nullptr }; }; + // + // SpawnAllEntitities + // + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_Call_AllEntitiesSpawned) { static constexpr size_t NumEntities = 4; @@ -96,7 +100,7 @@ namespace UnitTest size_t spawnedEntitiesCount = 0; auto callback = - [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities) + [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { spawnedEntitiesCount += entities.size(); }; @@ -106,6 +110,62 @@ namespace UnitTest EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash) + { + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriorty_Default); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // SpawnEntities + // + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash) + { + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriorty_Default, {}); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // DespawnAllEntities + // + + TEST_F(SpawnableEntitiesManagerTest, DespawnAllEntities_DeleteTicketBeforeCall_NoCrash) + { + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriorty_Default); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // ReloadSpawnable + // + + TEST_F(SpawnableEntitiesManagerTest, ReloadSpawnable_DeleteTicketBeforeCall_NoCrash) + { + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriorty_Default, *m_spawnableAsset); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // ListEntitities + // + TEST_F(SpawnableEntitiesManagerTest, ListEntities_Call_AllEntitiesAreReported) { static constexpr size_t NumEntities = 4; @@ -114,7 +174,7 @@ namespace UnitTest bool allValidEntityIds = true; size_t spawnedEntitiesCount = 0; auto callback = [&allValidEntityIds, &spawnedEntitiesCount] - (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView entities) + (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) { for (auto&& entity : entities) { @@ -131,6 +191,22 @@ namespace UnitTest EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + TEST_F(SpawnableEntitiesManagerTest, ListEntities_DeleteTicketBeforeCall_NoCrash) + { + auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) {}; + + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->ListEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // ListIndicesAndEntities + // + TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_Call_AllEntitiesAreReportedAndIncrementByOne) { static constexpr size_t NumEntities = 4; @@ -139,7 +215,7 @@ namespace UnitTest bool allValidEntityIds = true; size_t spawnedEntitiesCount = 0; auto callback = [&allValidEntityIds, &spawnedEntitiesCount] - (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstIndexEntityContainerView entities) + (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView entities) { for (auto&& indexEntityPair : entities) { @@ -160,6 +236,54 @@ namespace UnitTest EXPECT_EQ(NumEntities, spawnedEntitiesCount); } + TEST_F(SpawnableEntitiesManagerTest, ListIndicesAndEntities_DeleteTicketBeforeCall_NoCrash) + { + auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstIndexEntityContainerView) {}; + + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // ClaimEntities + // + + TEST_F(SpawnableEntitiesManagerTest, ClaimEntities_DeleteTicketBeforeCall_NoCrash) + { + auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableEntityContainerView) {}; + + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // Barrier + // + + TEST_F(SpawnableEntitiesManagerTest, Barrier_DeleteTicketBeforeCall_NoCrash) + { + auto callback = [](AzFramework::EntitySpawnTicket::Id) {}; + + { + AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); + m_manager->Barrier(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + } + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + } + + + // + // Misc. - Priority tests + // + TEST_F(SpawnableEntitiesManagerTest, Priority_HighBeforeDefault_HigherPriorityCallHappensBeforeDefaultPriorityEvenWhenQueuedLater) { static constexpr size_t NumEntities = 4; @@ -171,12 +295,12 @@ namespace UnitTest size_t highPriorityCallId = 0; size_t defaultPriorityCallId = 0; auto highCallback = [&callCounter, &highPriorityCallId] - (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) { highPriorityCallId = callCounter++; }; auto defaultCallback = [&callCounter, &defaultPriorityCallId] - (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) + (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) { defaultPriorityCallId = callCounter++; }; @@ -199,15 +323,15 @@ namespace UnitTest size_t highPriorityCallId = 0; size_t defaultPriorityCallId = 0; auto highCallback = - [&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) - { - highPriorityCallId = callCounter++; - }; + [&callCounter, &highPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) + { + highPriorityCallId = callCounter++; + }; auto defaultCallback = - [&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket&, AzFramework::SpawnableConstEntityContainerView) - { - defaultPriorityCallId = callCounter++; - }; + [&callCounter, &defaultPriorityCallId](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView) + { + defaultPriorityCallId = callCounter++; + }; m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); From 1248dc5fb43889530de28bc38feca79fd75ca414 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Wed, 26 May 2021 16:39:24 -0700 Subject: [PATCH 12/53] Spawning priority threshold through SetReg The priority threshold to consider a task high priority can now be configured through the Settings Registry under key "/O3DE/AzFramework/Spawnables/HighPriorityThreshold". --- .../Spawnable/SpawnableEntitiesManager.cpp | 34 +++++++------------ .../Spawnable/SpawnableEntitiesManager.h | 10 ++---- Registry/prefab.setreg | 14 ++++++++ 3 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 Registry/prefab.setreg diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index caf1112e9b..2c80aa8cbd 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -10,9 +10,11 @@ * */ +#include #include #include #include +#include #include #include #include @@ -26,7 +28,7 @@ namespace AzFramework void SpawnableEntitiesManager::QueueRequest(EntitySpawnTicket& ticket, SpawnablePriority priority, T&& request) { request.m_ticket = &GetTicketPayload(ticket); - Queue& queue = priority <= HighPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue; + Queue& queue = priority <= m_highPriorityThreshold ? m_highPriorityQueue : m_regularPriorityQueue; { AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex); request.m_requestId = GetTicketPayload(ticket).m_nextRequestId++; @@ -34,6 +36,16 @@ namespace AzFramework } } + SpawnableEntitiesManager::SpawnableEntitiesManager() + { + if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) + { + AZ::u64 value = 64; + settingsRegistry->Get(value, "/O3DE/AzFramework/Spawnables/HighPriorityThreshold"); + m_highPriorityThreshold = aznumeric_cast(AZStd::clamp(value, 0llu, 255llu)); + } + } + void SpawnableEntitiesManager::SpawnAllEntities( EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback) @@ -608,24 +620,4 @@ namespace AzFramework return false; } } - - bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs) - { - return GetTicketPayload(lhs) == GetTicketPayload(rhs); - } - - bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs) - { - return lhs == GetTicketPayload(rhs); - } - - bool SpawnableEntitiesManager::IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs) - { - return GetTicketPayload(lhs) == rhs; - } - - bool SpawnableEntitiesManager::IsEqualTicket(const Ticket* lhs, const Ticket* rhs) - { - return lhs = rhs; - } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index b98be60145..373a4db9cb 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -50,8 +50,7 @@ namespace AzFramework Regular = 1 << 1 }; - static constexpr SpawnablePriority HighPriorityThreshold = SpawnablePriority { 64 }; - + SpawnableEntitiesManager(); ~SpawnableEntitiesManager() override = default; // @@ -202,16 +201,13 @@ namespace AzFramework bool ProcessRequest(BarrierCommand& request, AZ::SerializeContext& serializeContext); bool ProcessRequest(DestroyTicketCommand& request, AZ::SerializeContext& serializeContext); - [[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const EntitySpawnTicket* rhs); - [[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const EntitySpawnTicket* rhs); - [[nodiscard]] static bool IsEqualTicket(const EntitySpawnTicket* lhs, const Ticket* rhs); - [[nodiscard]] static bool IsEqualTicket(const Ticket* lhs, const Ticket* rhs); - Queue m_highPriorityQueue; Queue m_regularPriorityQueue; AZ::Event> m_onSpawnedEvent; AZ::Event> m_onDespawnedEvent; + + SpawnablePriority m_highPriorityThreshold { 64 }; }; AZ_DEFINE_ENUM_BITWISE_OPERATORS(AzFramework::SpawnableEntitiesManager::CommandQueuePriority); diff --git a/Registry/prefab.setreg b/Registry/prefab.setreg new file mode 100644 index 0000000000..903dc0c4f8 --- /dev/null +++ b/Registry/prefab.setreg @@ -0,0 +1,14 @@ +{ + "O3DE": + { + "AzFramework": + { + "Spawnables": + { + // Any requests with a priorty value equal or smaller than this will be considered a high priority request. + // The range for this value is between 0 and 255. + "HighPriorityThreshold" : 64 + } + } + } +} \ No newline at end of file From c946d579282340877d8e11cba758a82b38e37cb8 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 27 May 2021 11:14:21 -0700 Subject: [PATCH 13/53] Addressed PR feedback. --- .../Spawnable/SpawnableEntitiesContainer.cpp | 12 +++---- .../Spawnable/SpawnableEntitiesInterface.cpp | 2 +- .../Spawnable/SpawnableEntitiesInterface.h | 16 ++++----- .../Spawnable/SpawnableEntitiesManager.h | 4 +++ .../Spawnable/SpawnableSystemComponent.cpp | 4 +-- .../SpawnableEntitiesManagerTests.cpp | 34 +++++++++---------- 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp index 9b06eb1f20..808de74e71 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesContainer.cpp @@ -38,20 +38,20 @@ namespace AzFramework void SpawnableEntitiesContainer::SpawnAllEntities() { AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); + SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default); } void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector entityIndices) { AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set."); SpawnableEntitiesInterface::Get()->SpawnEntities( - m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(entityIndices)); + m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(entityIndices)); } void SpawnableEntitiesContainer::DespawnAllEntities() { AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); - SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default); + SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default); } void SpawnableEntitiesContainer::Reset(AZ::Data::Asset spawnable) @@ -69,7 +69,7 @@ namespace AzFramework SpawnableEntitiesInterface::Get()->Barrier( m_threadData->m_spawnedEntitiesTicket, - SpawnablePriorty_Default, + SpawnablePriority_Default, [threadData = m_threadData](EntitySpawnTicket::Id) mutable { threadData.reset(); @@ -88,7 +88,7 @@ namespace AzFramework AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set."); SpawnableEntitiesInterface::Get()->Barrier( m_threadData->m_spawnedEntitiesTicket, - SpawnablePriorty_Default, + SpawnablePriority_Default, [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id) { callback(generation); @@ -116,6 +116,6 @@ namespace AzFramework AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str()); SpawnableEntitiesInterface::Get()->ReloadSpawnable( - m_threadData->m_spawnedEntitiesTicket, SpawnablePriorty_Default, AZStd::move(replacementAsset)); + m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(replacementAsset)); } } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp index 26a10933b5..ad1bf032c8 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp @@ -275,7 +275,7 @@ namespace AzFramework return *this; } - uint64_t EntitySpawnTicket::GetId() const + auto EntitySpawnTicket::GetId() const -> Id { return m_id; } diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h index b40136def7..27f45064b6 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h @@ -27,11 +27,11 @@ namespace AzFramework { AZ_TYPE_SAFE_INTEGRAL(SpawnablePriority, uint8_t); - inline static constexpr SpawnablePriority SpawnablePriorty_Highest { 0 }; - inline static constexpr SpawnablePriority SpawnablePriorty_High { 32 }; - inline static constexpr SpawnablePriority SpawnablePriorty_Default { 128 }; - inline static constexpr SpawnablePriority SpawnablePriorty_Low { 192 }; - inline static constexpr SpawnablePriority SpawnablePriorty_Lowest { 255 }; + inline static constexpr SpawnablePriority SpawnablePriority_Highest { 0 }; + inline static constexpr SpawnablePriority SpawnablePriority_High { 32 }; + inline static constexpr SpawnablePriority SpawnablePriority_Default { 128 }; + inline static constexpr SpawnablePriority SpawnablePriority_Low { 192 }; + inline static constexpr SpawnablePriority SpawnablePriority_Lowest { 255 }; class SpawnableEntityContainerView { @@ -154,7 +154,7 @@ namespace AzFramework EntitySpawnTicket& operator=(const EntitySpawnTicket&) = delete; EntitySpawnTicket& operator=(EntitySpawnTicket&& rhs); - uint64_t GetId() const; + Id GetId() const; bool IsValid() const; private: @@ -179,11 +179,11 @@ namespace AzFramework //! Calls on the same ticket are guaranteed to be executed in the order they are issued. Note that when issuing requests from //! multiple threads on the same ticket the order in which the requests are assigned to the ticket is not guaranteed. //! - //! Most calls have a priority where values closer to 0 mean higher priority than values closer to 255. The implementation of this + //! Most calls have a priority with values that range from 0 (highest priority) to 255 (lowest priority). The implementation of this //! interface may choose to use priority lanes which doesn't guarantee that higher priority requests happen before lower priority //! requests if they don't pass the priority lane threshold. Priority lanes and their thresholds are implementation specific and may //! differ between platforms. Note that if a call happened on a ticket with lower priority followed by a one with a higher priority - //! the first lower priority call will still needs to complete before the second higher priority call can be executed and the priority + //! the first lower priority call will still need to complete before the second higher priority call can be executed and the priority //! of the first call will not be updated. class SpawnableEntitiesDefinition { diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h index 373a4db9cb..afffdab8b5 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h @@ -207,6 +207,10 @@ namespace AzFramework AZ::Event> m_onSpawnedEvent; AZ::Event> m_onDespawnedEvent; + //! The threshold used to determine if a request goes in the regular (if bigger than the value) or high priority queue (if smaller + //! or equal to this value). The starting value of 64 is chosen as it's between default values SpawnablePriority_High and + //! SpawnablePriority_Default which gives users a bit of room to fine tune the priorities as this value can be configured + //! through the Settings Registry under the key "/O3DE/AzFramework/Spawnables/HighPriorityThreshold". SpawnablePriority m_highPriorityThreshold { 64 }; }; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp index 32cc61914e..300ff1441e 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableSystemComponent.cpp @@ -60,8 +60,8 @@ namespace AzFramework void SpawnableSystemComponent::OnSystemTick() { - // Handle only high priority spawning events such as those created from network. These need to happen even if the server - // doesn't have focus to avoid + // Handle only high priority spawning events such as those created from network. These need to happen even if the client + // doesn't have focus to avoid time-out issues for instance. m_entitiesManager.ProcessQueue(SpawnableEntitiesManager::CommandQueuePriority::High); } diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index 320e2ec435..484b7f46d7 100644 --- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -104,7 +104,7 @@ namespace UnitTest { spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(callback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(callback)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_EQ(NumEntities, spawnedEntitiesCount); @@ -114,7 +114,7 @@ namespace UnitTest { { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriorty_Default); + m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriority_Default); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -128,7 +128,7 @@ namespace UnitTest { { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriorty_Default, {}); + m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriority_Default, {}); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -142,7 +142,7 @@ namespace UnitTest { { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriorty_Default); + m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriority_Default); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -156,7 +156,7 @@ namespace UnitTest { { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriorty_Default, *m_spawnableAsset); + m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriority_Default, *m_spawnableAsset); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -183,8 +183,8 @@ namespace UnitTest spawnedEntitiesCount += entities.size(); }; - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); - m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default); + m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); @@ -197,7 +197,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ListEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -228,8 +228,8 @@ namespace UnitTest } }; - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default); - m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default); + m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); EXPECT_TRUE(allValidEntityIds); @@ -242,7 +242,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -258,7 +258,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -274,7 +274,7 @@ namespace UnitTest { AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset); - m_manager->Barrier(ticket, AzFramework::SpawnablePriorty_Default, AZStd::move(callback)); + m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback)); } m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } @@ -305,8 +305,8 @@ namespace UnitTest defaultPriorityCallId = callCounter++; }; - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); - m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback)); m_manager->ProcessQueue( AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); @@ -333,8 +333,8 @@ namespace UnitTest defaultPriorityCallId = callCounter++; }; - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_Default, {}, AZStd::move(defaultCallback)); - m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriorty_High, {}, AZStd::move(highCallback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback)); + m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback)); m_manager->ProcessQueue( AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High | AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); From 2550c3e1ff1f9cfb2241a1c3e488ef6091d77168 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 27 May 2021 16:13:32 -0700 Subject: [PATCH 14/53] Spawnable Entity Manager threshold default to "m_highPriorityThreshold" --- .../AzFramework/Spawnable/SpawnableEntitiesManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index 2c80aa8cbd..7b767d2a72 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -40,7 +40,7 @@ namespace AzFramework { if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) { - AZ::u64 value = 64; + AZ::u64 value = aznumeric_caster(m_highPriorityThreshold); settingsRegistry->Get(value, "/O3DE/AzFramework/Spawnables/HighPriorityThreshold"); m_highPriorityThreshold = aznumeric_cast(AZStd::clamp(value, 0llu, 255llu)); } From d4ce2849c7995b24311a973d731dc85fb33f82ad Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 27 May 2021 16:46:33 -0700 Subject: [PATCH 15/53] Post-merge fixup. --- .../Code/Source/Pipeline/NetBindMarkerComponent.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp index 1696e851a5..c93c09cfbc 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetBindMarkerComponent.cpp @@ -59,7 +59,7 @@ namespace Multiplayer AZ::Transform worldTm = GetEntity()->FindComponent()->GetWorldTM(); auto preInsertionCallback = [worldTm = AZStd::move(worldTm), netEntityIndex = m_netEntityIndex, spawnableAssetId = m_networkSpawnableAsset.GetId()] - (AzFramework::EntitySpawnTicket&, AzFramework::SpawnableEntityContainerView entities) + (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableEntityContainerView entities) { if (entities.size() == 1) { @@ -81,7 +81,8 @@ namespace Multiplayer }; m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset); - AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(m_netSpawnTicket, {m_netEntityIndex}, preInsertionCallback); + AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities( + m_netSpawnTicket, AzFramework::SpawnablePriority_Default, { m_netEntityIndex }, preInsertionCallback); } } @@ -89,7 +90,7 @@ namespace Multiplayer { if(m_netSpawnTicket.IsValid()) { - AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket); + AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket, AzFramework::SpawnablePriority_Default); } } From c45697fd510efd95fb41f861799763b9998aae17 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 27 May 2021 16:57:15 -0700 Subject: [PATCH 16/53] Fixed unit test compile error with spawnables --- .../Tests/Prefab/Benchmark/SpawnableCreateBenchmarks.cpp | 3 ++- .../AzToolsFramework/Tests/Prefab/SpawnableCreateTests.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/SpawnableCreateBenchmarks.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/SpawnableCreateBenchmarks.cpp index 821daaba82..8e4c2eaad7 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/SpawnableCreateBenchmarks.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/Benchmark/SpawnableCreateBenchmarks.cpp @@ -34,7 +34,8 @@ namespace Benchmark { state.PauseTiming(); - auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom); + AzFramework::Spawnable spawnable; + AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom); state.ResumeTiming(); } diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/SpawnableCreateTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/SpawnableCreateTests.cpp index 2e1cd14b2a..af178ec7a4 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/SpawnableCreateTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/SpawnableCreateTests.cpp @@ -40,7 +40,8 @@ namespace UnitTest //Create Spawnable auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(instance->GetTemplateId()); - auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom); + AzFramework::Spawnable spawnable; + AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom); EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity const auto& spawnableEntities = spawnable.GetEntities(); @@ -84,7 +85,8 @@ namespace UnitTest //Create Spawnable auto& prefabDom = m_prefabSystemComponent->FindTemplateDom(thirdInstance->GetTemplateId()); - auto spawnable = ::AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(prefabDom); + AzFramework::Spawnable spawnable; + AzToolsFramework::Prefab::SpawnableUtils::CreateSpawnable(spawnable, prefabDom); EXPECT_EQ(spawnable.GetEntities().size() - 1, normalEntityCount); // 1 for container entity const auto& spawnableEntities = spawnable.GetEntities(); From 0f258954fbd8a0bd2279b4a29369dc0667c3f2bf Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Thu, 27 May 2021 22:29:40 -0700 Subject: [PATCH 17/53] Fix for unit test. Checking that AssetManager is ready before spawning entities; MultiplayerSystemComponent will attempt to spawn a default player on init(), but during unit tests the AssetManager isn't stood up --- .../Code/Source/NetworkEntity/NetworkEntityManager.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index dda3f18ad4..eaf89f3489 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -402,8 +402,12 @@ namespace Multiplayer const AZ::Transform& transform ) { - INetworkEntityManager::EntityList returnList; - + EntityList returnList; + if (!AZ::Data::AssetManager::IsReady()) + { + return returnList; + } + auto spawnableAssetId = m_networkPrefabLibrary.GetAssetIdByName(prefabEntryId.m_prefabName); // Required for sync-instantiation. Todo: keep the reference in NetworkSpawnableLibrary auto netSpawnableAsset = AZ::Data::AssetManager::Instance().GetAsset(spawnableAssetId, AZ::Data::AssetLoadBehavior::PreLoad); From fdc57cdaff7138f0e576978ce867ff6b099300fb Mon Sep 17 00:00:00 2001 From: Doug McDiarmid Date: Fri, 28 May 2021 00:08:13 -0700 Subject: [PATCH 18/53] Added TLAS dependency --- .../ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli | 2 +- .../Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli index 2352f5d09b..b8c97ef421 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSceneSrg.azsli @@ -143,7 +143,7 @@ ShaderResourceGroup RayTracingSceneSrg : SRG_RayTracingScene float4 m_irradianceColor; float3x3 m_worldInvTranspose; - float m_padding1[1]; + float m_padding1; uint m_bufferFlags; uint m_bufferStartIndex; diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h index d89b61b2f9..90a9383ae6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h @@ -236,7 +236,7 @@ namespace AZ AZStd::array m_irradianceColor; // float4 AZStd::array m_worldInvTranspose; // float3x3 - float m_padding1[1]; + float m_padding1; RayTracingSubMeshBufferFlags m_bufferFlags = RayTracingSubMeshBufferFlags::None; uint32_t m_bufferStartIndex = 0; From 9ec4278f86d517c6ac830c2e60f106abe1bf891a Mon Sep 17 00:00:00 2001 From: Doug McDiarmid Date: Fri, 28 May 2021 00:09:00 -0700 Subject: [PATCH 19/53] Added TLAS dependency --- .../Code/Source/RayTracing/RayTracingPass.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp index 1ec2d9ae04..988870cc0e 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingPass.cpp @@ -196,8 +196,36 @@ namespace AZ void RayTracingPass::SetupFrameGraphDependencies(RHI::FrameGraphInterface frameGraph) { + RPI::Scene* scene = m_pipeline->GetScene(); + RayTracingFeatureProcessor* rayTracingFeatureProcessor = scene->GetFeatureProcessor(); + AZ_Assert(rayTracingFeatureProcessor, "RayTracingPass requires the RayTracingFeatureProcessor"); + RPI::RenderPass::SetupFrameGraphDependencies(frameGraph); frameGraph.SetEstimatedItemCount(1); + + // TLAS + { + const RHI::Ptr& rayTracingTlasBuffer = rayTracingFeatureProcessor->GetTlas()->GetTlasBuffer(); + if (rayTracingTlasBuffer) + { + AZ::RHI::AttachmentId tlasAttachmentId = rayTracingFeatureProcessor->GetTlasAttachmentId(); + if (frameGraph.GetAttachmentDatabase().IsAttachmentValid(tlasAttachmentId) == false) + { + [[maybe_unused]] RHI::ResultCode result = frameGraph.GetAttachmentDatabase().ImportBuffer(tlasAttachmentId, rayTracingTlasBuffer); + AZ_Assert(result == RHI::ResultCode::Success, "Failed to import ray tracing TLAS buffer with error %d", result); + } + + uint32_t tlasBufferByteCount = aznumeric_cast(rayTracingFeatureProcessor->GetTlas()->GetTlasBuffer()->GetDescriptor().m_byteCount); + RHI::BufferViewDescriptor tlasBufferViewDescriptor = RHI::BufferViewDescriptor::CreateRaw(0, tlasBufferByteCount); + + RHI::BufferScopeAttachmentDescriptor desc; + desc.m_attachmentId = tlasAttachmentId; + desc.m_bufferViewDescriptor = tlasBufferViewDescriptor; + desc.m_loadStoreAction.m_loadAction = AZ::RHI::AttachmentLoadAction::Load; + + frameGraph.UseShaderAttachment(desc, RHI::ScopeAttachmentAccess::ReadWrite); + } + } } void RayTracingPass::CompileResources(const RHI::FrameGraphCompileContext& context) From bdf9da820dac872d39077c28f11e383346a1ec27 Mon Sep 17 00:00:00 2001 From: gallowj Date: Fri, 28 May 2021 09:49:44 -0500 Subject: [PATCH 20/53] removing folder with image data we don't own, can't license. --- .../sampleEnvironment/PaperMill_E_3k.exr | 3 - .../PaperMill_E_3k.exr.assetinfo | 69 ------------------- .../sampleEnvironment/exampleBrdf_lut.dds | 3 - .../exampleDiffuseHDR_cm.dds | 3 - .../exampleSpecularHDR_cm.dds | 3 - .../sampleEnvironment/example_iblskyboxcm.dds | 3 - .../sampleEnvironment/papermill_license.txt | 10 --- 7 files changed, 94 deletions(-) delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr.assetinfo delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleBrdf_lut.dds delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleDiffuseHDR_cm.dds delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleSpecularHDR_cm.dds delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/example_iblskyboxcm.dds delete mode 100644 Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/papermill_license.txt diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr deleted file mode 100644 index 0fcbcc4746..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc9981393c88c6d30a0a5a6837e6f6246a9f550042b3c4be39dd34e479b90569 -size 16793931 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr.assetinfo b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr.assetinfo deleted file mode 100644 index 16cb0dd668..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/PaperMill_E_3k.exr.assetinfo +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleBrdf_lut.dds b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleBrdf_lut.dds deleted file mode 100644 index f2b2ce550d..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleBrdf_lut.dds +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:440bcb1579d4ad667c040bda914ed3121980526a94f23879a0c482c799fd5132 -size 1310848 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleDiffuseHDR_cm.dds b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleDiffuseHDR_cm.dds deleted file mode 100644 index 9585c25dd1..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleDiffuseHDR_cm.dds +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf4b22481726214c062ac27fb2d9d8a49e760a76d8fff03330ff9c5f9275a8da -size 1966208 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleSpecularHDR_cm.dds b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleSpecularHDR_cm.dds deleted file mode 100644 index a35fac45fb..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/exampleSpecularHDR_cm.dds +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b4c99faffc34988c268613948f2004f40e9bd51de915461a4cb74edc5e8bae6 -size 134217920 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/example_iblskyboxcm.dds b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/example_iblskyboxcm.dds deleted file mode 100644 index a89dfdbd3d..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/example_iblskyboxcm.dds +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7edea42ded8143764654f12c19e0c9b74c74afb21f435ebb59c0a4d203892a3 -size 536871104 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/papermill_license.txt b/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/papermill_license.txt deleted file mode 100644 index 83cfe08ab9..0000000000 --- a/Gems/Atom/Feature/Common/Assets/Textures/sampleEnvironment/papermill_license.txt +++ /dev/null @@ -1,10 +0,0 @@ -The papermill 'Image base lighting' (IBL) images are modified from the following: - -http://www.hdrlabs.com/sibl/archive.html -'Papermill Ruins E' - -All sIBL-sets on this page, including the images within, are licensed under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 License. - -Creative Commons License: http://creativecommons.org/licenses/by-nc-sa/3.0/us/ - -Remember: Do what you want with them, but always mention where you got them from... \ No newline at end of file From de4e6957e8606fb3ca7fa49cc0fefaf81f8af357 Mon Sep 17 00:00:00 2001 From: Chris Santora Date: Fri, 28 May 2021 13:38:56 -0700 Subject: [PATCH 21/53] Made a bunch of display name and description updates to core material types. - Renamed "Details" group to "Overview". - Renamed "UV Names" group to "UV Sets". - Renamed "General" group to "General Settings". - Renamed "Parallax" group to "Displacement". - Renamed "Texture Map" properties to just "Texture". In cases where a specific type of texture is mentioned like "roughness texture map" I called this "roughness map" (which is more common according to google). - Renamed "Heightmap" to "Height map" (which is more common according to google). ATOM-14002 [Material Editor] Revisit user facing organization and layout of material types --- .../Materials/Types/EnhancedPBR.materialtype | 128 ++++----- .../Assets/Materials/Types/Skin.materialtype | 72 ++--- .../Types/StandardMultilayerPBR.materialtype | 256 +++++++++--------- .../Materials/Types/StandardPBR.materialtype | 114 ++++---- .../RPI.Edit/Material/MaterialSourceData.h | 2 +- .../Document/MaterialDocumentRequestBus.h | 2 +- .../Code/Source/Document/MaterialDocument.cpp | 10 +- .../MaterialInspector/MaterialInspector.cpp | 16 +- .../MaterialInspector/MaterialInspector.h | 2 +- .../EditorMaterialComponentInspector.cpp | 2 +- 10 files changed, 302 insertions(+), 302 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index 3696188514..3b5a653f43 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -60,8 +60,8 @@ }, { "id": "parallax", - "displayName": "Parallax Mapping", - "description": "Properties for parallax effect produced by depthmap." + "displayName": "Displacement", + "description": "Properties for parallax effect produced by a height map." }, { "id": "subsurfaceScattering", @@ -86,7 +86,7 @@ }, { "id": "general", - "displayName": "General", + "displayName": "General Settings", "description": "General settings." } ], @@ -197,7 +197,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -208,14 +208,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -227,7 +227,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -253,7 +253,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "", "type": "Image", "connection": { @@ -264,14 +264,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Metallic texture map UV set", + "description": "Metallic map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -284,8 +284,8 @@ "roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -295,14 +295,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -315,7 +315,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -329,7 +329,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -409,8 +409,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -420,14 +420,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -472,7 +472,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Strength factor texture map", + "description": "Strength factor texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -482,14 +482,14 @@ { "id": "useInfluenceMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "influenceMapUv", "displayName": " UV", - "description": "Strength factor texture map UV set", + "description": "Strength factor map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -514,7 +514,7 @@ { "id": "roughnessMap", "displayName": " Roughness Map", - "description": "Roughness texture map", + "description": "Texture for defining surface roughness", "type": "Image", "connection": { "type": "ShaderInput", @@ -524,14 +524,14 @@ { "id": "useRoughnessMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the roughness value.", + "description": "Whether to use the texture, or just default to the roughness value.", "type": "Bool", "defaultValue": true }, { "id": "roughnessMapUv", "displayName": " UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -573,7 +573,7 @@ { "id": "normalMapUv", "displayName": " UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -586,8 +586,8 @@ "normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -597,14 +597,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -653,7 +653,7 @@ { "id": "mode", "displayName": "Opacity Mode", - "description": "Opacity mode for this texture.", + "description": "Indicates the general approach how transparency is to be applied.", "type": "Enum", "enumValues": [ "Opaque", "Cutout", "Blended", "TintedTransparent" ], "defaultValue": "Opaque", @@ -665,7 +665,7 @@ { "id": "alphaSource", "displayName": "Alpha Source", - "description": "Source texture of alpha value.", + "description": "Indicates whether to get the opacity texture from the Base Color map (Packed) or from a separate greyscale texture (Split).", "type": "Enum", "enumValues": [ "Packed", "Split", "None" ], "defaultValue": "Packed", @@ -676,8 +676,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface opacity.", + "displayName": "Texture", + "description": "Texture for defining surface opacity.", "type": "Image", "connection": { "type": "ShaderInput", @@ -687,7 +687,7 @@ { "id": "textureMapUv", "displayName": "UV", - "description": "Opacity texture map UV set", + "description": "Opacity map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -782,7 +782,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -792,14 +792,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -824,7 +824,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -834,14 +834,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -904,8 +904,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining emissive area.", + "displayName": "Texture", + "description": "Texture for defining emissive area.", "type": "Image", "connection": { "type": "ShaderInput", @@ -915,14 +915,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Emissive texture map UV set", + "description": "Emissive map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -935,8 +935,8 @@ "parallax": [ { "id": "textureMap", - "displayName": "Heightmap", - "description": "Displacement heightmap to create parallax effect.", + "displayName": "Height Map", + "description": "Displacement height map to create parallax effect.", "type": "Image", "connection": { "type": "ShaderInput", @@ -946,14 +946,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the heightmap.", + "description": "Whether to use the height map.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Heightmap UV set", + "description": "Height map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -964,8 +964,8 @@ }, { "id": "factor", - "displayName": "Heightmap Scale", - "description": "The total height of the heightmap in local model units.", + "displayName": "Height Map Scale", + "description": "The total height of the height map in local model units.", "type": "Float", "defaultValue": 0.05, "min": 0.0, @@ -1026,7 +1026,7 @@ { "id": "showClipping", "displayName": "Show Clipping", - "description": "Highlight areas where the heightmap is clipped by the mesh surface.", + "description": "Highlight areas where the height map is clipped by the mesh surface.", "type": "Bool", "defaultValue": false, "connection": { @@ -1063,7 +1063,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Use texture map to control the strength of subsurface scattering", + "description": "Texture for controlling the strength of subsurface scattering", "type": "Image", "connection": { "type": "ShaderInput", @@ -1073,7 +1073,7 @@ { "id": "useInfluenceMap", "displayName": " Use Influence Map", - "description": "Whether to use the texture map as influence mask.", + "description": "Whether to use the influence map.", "type": "Bool", "defaultValue": true }, @@ -1142,7 +1142,7 @@ { "id": "thicknessMap", "displayName": " Thickness Map", - "description": "Use a greyscale texture for per pixel thickness", + "description": "Texture for controlling per pixel thickness", "type": "Image", "connection": { "type": "ShaderInput", @@ -1171,7 +1171,7 @@ { "id": "transmissionTint", "displayName": " Transmission Tint", - "description": "Color of the volume light travelling through", + "description": "Color of the volume light traveling through", "type": "Color", "defaultValue": [ 1.0, 0.8, 0.6 ] }, @@ -1246,7 +1246,7 @@ { "id": "enableDetailMaskTexture", "displayName": " Use Texture", - "description": "Enable detail mask texture", + "description": "Enable detail blend mask", "type": "Bool", "defaultValue": true }, @@ -1265,7 +1265,7 @@ { "id": "textureMapUv", "displayName": "Detail Map UVs", - "description": "Which UV set to use for detail map texture sampling", + "description": "Which UV set to use for detail map sampling", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1283,8 +1283,8 @@ }, { "id": "baseColorDetailMap", - "displayName": " Texture Map", - "description": "Detailed Base Color Texture map", + "displayName": " Texture", + "description": "Detailed Base Color Texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -1307,7 +1307,7 @@ { "id": "enableNormals", "displayName": "Enable Normal", - "description": "Enable detail normal texture to be used for fine detail normal such as scratches and small dents", + "description": "Enable detail normal map to be used for fine detail normal such as scratches and small dents", "type": "Bool", "defaultValue": false }, @@ -1326,8 +1326,8 @@ }, { "id": "normalDetailMap", - "displayName": " Texture Map", - "description": "Detailed Normal Texture map", + "displayName": " Texture", + "description": "Detailed Normal map", "type": "Image", "connection": { "type": "ShaderInput", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index f8c49d579c..ab36853723 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -50,7 +50,7 @@ }, { "id": "general", - "displayName": "General", + "displayName": "General Settings", "description": "General settings." } ], @@ -150,7 +150,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -161,14 +161,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Unwrapped", @@ -180,7 +180,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -193,8 +193,8 @@ "roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -204,14 +204,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Unwrapped", @@ -224,7 +224,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -238,7 +238,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -279,8 +279,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -290,14 +290,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Unwrapped", @@ -321,8 +321,8 @@ "normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -332,14 +332,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Unwrapped", @@ -388,7 +388,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -398,14 +398,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -430,7 +430,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -440,14 +440,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -498,7 +498,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Use texture map to control the strength of subsurface scattering", + "description": "Texture for controlling the strength of subsurface scattering", "type": "Image", "connection": { "type": "ShaderInput", @@ -508,7 +508,7 @@ { "id": "useInfluenceMap", "displayName": " Use Influence Map", - "description": "Whether to use the texture map as influence mask.", + "description": "Whether to use the influence map.", "type": "Bool", "defaultValue": true }, @@ -577,7 +577,7 @@ { "id": "thicknessMap", "displayName": " Thickness Map", - "description": "Use a greyscale texture for per pixel thickness", + "description": "Texture for controlling per pixel thickness", "type": "Image", "connection": { "type": "ShaderInput", @@ -606,7 +606,7 @@ { "id": "transmissionTint", "displayName": " Transmission Tint", - "description": "Color of the volume light travelling through", + "description": "Color of the volume light traveling through", "type": "Color", "defaultValue": [ 1.0, 0.8, 0.6 ] }, @@ -792,7 +792,7 @@ { "id": "enableDetailMaskTexture", "displayName": " Use Texture", - "description": "Enable detail mask texture", + "description": "Enable detail blend mask", "type": "Bool", "defaultValue": true }, @@ -811,7 +811,7 @@ { "id": "textureMapUv", "displayName": "Detail Map UVs", - "description": "Which UV set to use for detail map texture sampling", + "description": "Which UV set to use for detail map sampling", "type": "Enum", "enumIsUv": true, "defaultValue": "Unwrapped", @@ -829,8 +829,8 @@ }, { "id": "baseColorDetailMap", - "displayName": " Texture Map", - "description": "Detailed Base Color Texture map", + "displayName": " Texture", + "description": "Detailed Base Color Texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -853,7 +853,7 @@ { "id": "enableNormals", "displayName": "Enable Normal", - "description": "Enable detail normal texture to be used for fine detail normal such as scratches and small dents", + "description": "Enable detail normal map to be used for fine detail normal such as scratches and small dents", "type": "Bool", "defaultValue": false }, @@ -872,8 +872,8 @@ }, { "id": "normalDetailMap", - "displayName": " Texture Map", - "description": "Detailed Normal Texture map", + "displayName": " Texture", + "description": "Detailed Normal map", "type": "Image", "connection": { "type": "ShaderInput", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype index ca6cb77b0a..5b6e5f30bc 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype @@ -26,7 +26,7 @@ }, { "id": "general", - "displayName": "General", + "displayName": "General Settings", "description": "General settings." }, //############################################################################################## @@ -428,7 +428,7 @@ { "id": "showClipping", "displayName": "Show Clipping", - "description": "Highlight areas where the heightmap is clipped by the mesh surface.", + "description": "Highlight areas where the height map is clipped by the mesh surface.", "type": "Bool", "defaultValue": false, "connection": { @@ -550,7 +550,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -561,14 +561,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -580,7 +580,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -606,7 +606,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "", "type": "Image", "connection": { @@ -617,14 +617,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Metallic texture map UV set", + "description": "Metallic map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -637,8 +637,8 @@ "layer1_roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -648,14 +648,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -668,7 +668,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -682,7 +682,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -723,8 +723,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -734,14 +734,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -754,8 +754,8 @@ "layer1_normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -765,14 +765,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -841,7 +841,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Strength factor texture map", + "description": "Strength factor texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -851,14 +851,14 @@ { "id": "useInfluenceMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "influenceMapUv", "displayName": " UV", - "description": "Strength factor texture map UV set", + "description": "Strength factor map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -883,7 +883,7 @@ { "id": "roughnessMap", "displayName": " Roughness Map", - "description": "Roughness texture map", + "description": "Texture for defining surface roughness", "type": "Image", "connection": { "type": "ShaderInput", @@ -893,14 +893,14 @@ { "id": "useRoughnessMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the roughness value.", + "description": "Whether to use the texture, or just default to the roughness value.", "type": "Bool", "defaultValue": true }, { "id": "roughnessMapUv", "displayName": " UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -942,7 +942,7 @@ { "id": "normalMapUv", "displayName": " UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -956,7 +956,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -966,14 +966,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -998,7 +998,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1008,14 +1008,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1078,8 +1078,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining emissive area.", + "displayName": "Texture", + "description": "Texture for defining emissive area.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1089,14 +1089,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Emissive texture map UV set", + "description": "Emissive map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1109,8 +1109,8 @@ "layer1_parallax": [ { "id": "textureMap", - "displayName": "Heightmap", - "description": "Displacement heightmap, which can be used for layer blending and/or a parallax effect.", + "displayName": "Height Map", + "description": "Displacement height map, which can be used for layer blending and/or a parallax effect.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1120,14 +1120,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the heightmap.", + "description": "Whether to use the height map.", "type": "Bool", "defaultValue": true }, { "id": "factor", "displayName": "Scale", - "description": "The total height of the heightmap in local model units.", + "description": "The total height of the height map in local model units.", "type": "Float", "defaultValue": 0.05, "min": 0.0, @@ -1245,7 +1245,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -1256,14 +1256,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1275,7 +1275,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -1301,7 +1301,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "", "type": "Image", "connection": { @@ -1312,14 +1312,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Metallic texture map UV set", + "description": "Metallic map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1332,8 +1332,8 @@ "layer2_roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1343,14 +1343,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1363,7 +1363,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -1377,7 +1377,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -1418,8 +1418,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1429,14 +1429,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1449,8 +1449,8 @@ "layer2_normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1460,14 +1460,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1536,7 +1536,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Strength factor texture map", + "description": "Strength factor texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -1546,14 +1546,14 @@ { "id": "useInfluenceMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "influenceMapUv", "displayName": " UV", - "description": "Strength factor texture map UV set", + "description": "Strength factor map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1578,7 +1578,7 @@ { "id": "roughnessMap", "displayName": " Roughness Map", - "description": "Roughness texture map", + "description": "Texture for defining surface roughness", "type": "Image", "connection": { "type": "ShaderInput", @@ -1588,14 +1588,14 @@ { "id": "useRoughnessMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the roughness value.", + "description": "Whether to use the texture, or just default to the roughness value.", "type": "Bool", "defaultValue": true }, { "id": "roughnessMapUv", "displayName": " UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1637,7 +1637,7 @@ { "id": "normalMapUv", "displayName": " UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1651,7 +1651,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1661,14 +1661,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1693,7 +1693,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1703,14 +1703,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1773,8 +1773,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining emissive area.", + "displayName": "Texture", + "description": "Texture for defining emissive area.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1784,14 +1784,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Emissive texture map UV set", + "description": "Emissive map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1804,8 +1804,8 @@ "layer2_parallax": [ { "id": "textureMap", - "displayName": "Heightmap", - "description": "Displacement heightmap, which can be used for layer blending and/or a parallax effect.", + "displayName": "Height Map", + "description": "Displacement height map, which can be used for layer blending and/or a parallax effect.", "type": "Image", "connection": { "type": "ShaderInput", @@ -1815,14 +1815,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the heightmap.", + "description": "Whether to use the height map.", "type": "Bool", "defaultValue": true }, { "id": "factor", "displayName": "Scale", - "description": "The total height of the heightmap in local model units.", + "description": "The total height of the height map in local model units.", "type": "Float", "defaultValue": 0.05, "min": 0.0, @@ -1940,7 +1940,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -1951,14 +1951,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -1970,7 +1970,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -1996,7 +1996,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "", "type": "Image", "connection": { @@ -2007,14 +2007,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Metallic texture map UV set", + "description": "Metallic map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2027,8 +2027,8 @@ "layer3_roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2038,14 +2038,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2058,7 +2058,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -2072,7 +2072,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -2113,8 +2113,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2124,14 +2124,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2144,8 +2144,8 @@ "layer3_normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2155,14 +2155,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2231,7 +2231,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Strength factor texture map", + "description": "Strength factor texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -2241,14 +2241,14 @@ { "id": "useInfluenceMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "influenceMapUv", "displayName": " UV", - "description": "Strength factor texture map UV set", + "description": "Strength factor map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2273,7 +2273,7 @@ { "id": "roughnessMap", "displayName": " Roughness Map", - "description": "Roughness texture map", + "description": "Texture for defining surface roughness", "type": "Image", "connection": { "type": "ShaderInput", @@ -2283,14 +2283,14 @@ { "id": "useRoughnessMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the roughness value.", + "description": "Whether to use the texture, or just default to the roughness value.", "type": "Bool", "defaultValue": true }, { "id": "roughnessMapUv", "displayName": " UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2332,7 +2332,7 @@ { "id": "normalMapUv", "displayName": " UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2346,7 +2346,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2356,14 +2356,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2388,7 +2388,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2398,14 +2398,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2468,8 +2468,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining emissive area.", + "displayName": "Texture", + "description": "Texture for defining emissive area.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2479,14 +2479,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Emissive texture map UV set", + "description": "Emissive map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -2499,8 +2499,8 @@ "layer3_parallax": [ { "id": "textureMap", - "displayName": "Heightmap", - "description": "Displacement heightmap, which can be used for layer blending and/or a parallax effect.", + "displayName": "Height Map", + "description": "Displacement height map, which can be used for layer blending and/or a parallax effect.", "type": "Image", "connection": { "type": "ShaderInput", @@ -2510,14 +2510,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the heightmap.", + "description": "Whether to use the height map.", "type": "Bool", "defaultValue": true }, { "id": "factor", "displayName": "Scale", - "description": "The total height of the heightmap in local model units.", + "description": "The total height of the height map in local model units.", "type": "Float", "defaultValue": 0.05, "min": 0.0, diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 183cddd4cb..0904302085 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -55,8 +55,8 @@ }, { "id": "parallax", - "displayName": "Parallax Mapping", - "description": "Properties for parallax effect produced by depthmap." + "displayName": "Displacement", + "description": "Properties for parallax effect produced by a height map." }, { "id": "subsurfaceScattering", @@ -71,7 +71,7 @@ }, { "id": "general", - "displayName": "General", + "displayName": "General Settings", "description": "General settings." } ], @@ -182,7 +182,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "Base color texture map", "type": "Image", "connection": { @@ -193,14 +193,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Base color texture map UV set", + "description": "Base color map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -212,7 +212,7 @@ { "id": "textureBlendMode", "displayName": "Texture Blend Mode", - "description": "Selects the equation to use when combining Color, Factor, and Texture Map.", + "description": "Selects the equation to use when combining Color, Factor, and Texture.", "type": "Enum", "enumValues": [ "Multiply", "LinearLight", "Lerp", "Overlay" ], "defaultValue": "Multiply", @@ -238,7 +238,7 @@ }, { "id": "textureMap", - "displayName": "Texture Map", + "displayName": "Texture", "description": "", "type": "Image", "connection": { @@ -249,14 +249,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Metallic texture map UV set", + "description": "Metallic map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -269,8 +269,8 @@ "roughness": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface roughness.", + "displayName": "Texture", + "description": "Texture for defining surface roughness.", "type": "Image", "connection": { "type": "ShaderInput", @@ -280,14 +280,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -300,7 +300,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "lowerBound", "displayName": "Lower Bound", - "description": "The roughness value that corresponds to black in the texture map.", + "description": "The roughness value that corresponds to black in the texture.", "type": "Float", "defaultValue": 0.0, "min": 0.0, @@ -314,7 +314,7 @@ // Note that "factor" is mutually exclusive with "lowerBound"/"upperBound". These are swapped by a lua functor. "id": "upperBound", "displayName": "Upper Bound", - "description": "The roughness value that corresponds to white in the texture map.", + "description": "The roughness value that corresponds to white in the texture.", "type": "Float", "defaultValue": 1.0, "min": 0.0, @@ -355,8 +355,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface reflectance.", + "displayName": "Texture", + "description": "Texture for defining surface reflectance.", "type": "Image", "connection": { "type": "ShaderInput", @@ -366,14 +366,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Specular reflection texture map UV set", + "description": "Specular reflection map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -418,7 +418,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Strength factor texture map", + "description": "Strength factor texture", "type": "Image", "connection": { "type": "ShaderInput", @@ -428,14 +428,14 @@ { "id": "useInfluenceMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the Factor value.", + "description": "Whether to use the texture, or just default to the Factor value.", "type": "Bool", "defaultValue": true }, { "id": "influenceMapUv", "displayName": " UV", - "description": "Strength factor texture map UV set", + "description": "Strength factor map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -460,7 +460,7 @@ { "id": "roughnessMap", "displayName": " Roughness Map", - "description": "Roughness texture map", + "description": "Texture for defining surface roughness", "type": "Image", "connection": { "type": "ShaderInput", @@ -470,14 +470,14 @@ { "id": "useRoughnessMap", "displayName": " Use Texture", - "description": "Whether to use the texture map, or just default to the roughness value.", + "description": "Whether to use the texture, or just default to the roughness value.", "type": "Bool", "defaultValue": true }, { "id": "roughnessMapUv", "displayName": " UV", - "description": "Roughness texture map UV set", + "description": "Roughness map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -519,7 +519,7 @@ { "id": "normalMapUv", "displayName": " UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -532,8 +532,8 @@ "normal": [ { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface normal direction.", + "displayName": "Texture", + "description": "Texture for defining surface normal direction.", "type": "Image", "connection": { "type": "ShaderInput", @@ -543,14 +543,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map, or just rely on vertex normals.", + "description": "Whether to use the texture, or just rely on vertex normals.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Normal texture map UV set", + "description": "Normal map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -599,7 +599,7 @@ { "id": "mode", "displayName": "Opacity Mode", - "description": "Opacity mode for this texture.", + "description": "Indicates the general approach how transparency is to be applied.", "type": "Enum", "enumValues": [ "Opaque", "Cutout", "Blended", "TintedTransparent" ], "defaultValue": "Opaque", @@ -611,7 +611,7 @@ { "id": "alphaSource", "displayName": "Alpha Source", - "description": "Source texture of alpha value.", + "description": "Indicates whether to get the opacity texture from the Base Color map (Packed) or from a separate greyscale texture (Split).", "type": "Enum", "enumValues": [ "Packed", "Split", "None" ], "defaultValue": "Packed", @@ -622,8 +622,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining surface opacity.", + "displayName": "Texture", + "description": "Texture for defining surface opacity.", "type": "Image", "connection": { "type": "ShaderInput", @@ -633,7 +633,7 @@ { "id": "textureMapUv", "displayName": "UV", - "description": "Opacity texture map UV set", + "description": "Opacity map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -728,7 +728,7 @@ { "id": "diffuseTextureMap", "displayName": "Diffuse AO", - "description": "Texture map for defining occlusion area for diffuse ambient lighting.", + "description": "Texture for defining occlusion area for diffuse ambient lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -738,14 +738,14 @@ { "id": "diffuseUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Diffuse AO texture map.", + "description": "Whether to use the Diffuse AO map.", "type": "Bool", "defaultValue": true }, { "id": "diffuseTextureMapUv", "displayName": " UV", - "description": "Diffuse AO texture map UV set.", + "description": "Diffuse AO map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -770,7 +770,7 @@ { "id": "specularTextureMap", "displayName": "Specular Cavity", - "description": "Texture map for defining occlusion area for specular lighting.", + "description": "Texture for defining occlusion area for specular lighting.", "type": "Image", "connection": { "type": "ShaderInput", @@ -780,14 +780,14 @@ { "id": "specularUseTexture", "displayName": " Use Texture", - "description": "Whether to use the Specular Cavity texture map.", + "description": "Whether to use the Specular Cavity map.", "type": "Bool", "defaultValue": true }, { "id": "specularTextureMapUv", "displayName": " UV", - "description": "Specular Cavity texture map UV set.", + "description": "Specular Cavity map UV set.", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -850,8 +850,8 @@ }, { "id": "textureMap", - "displayName": "Texture Map", - "description": "Texture map for defining emissive area.", + "displayName": "Texture", + "description": "Texture for defining emissive area.", "type": "Image", "connection": { "type": "ShaderInput", @@ -861,14 +861,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the texture map.", + "description": "Whether to use the texture.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Emissive texture map UV set", + "description": "Emissive map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -881,8 +881,8 @@ "parallax": [ { "id": "textureMap", - "displayName": "Heightmap", - "description": "Displacement heightmap to create parallax effect.", + "displayName": "Height Map", + "description": "Displacement height map to create parallax effect.", "type": "Image", "connection": { "type": "ShaderInput", @@ -892,14 +892,14 @@ { "id": "useTexture", "displayName": "Use Texture", - "description": "Whether to use the heightmap.", + "description": "Whether to use the height map.", "type": "Bool", "defaultValue": true }, { "id": "textureMapUv", "displayName": "UV", - "description": "Heightmap UV set", + "description": "Height map UV set", "type": "Enum", "enumIsUv": true, "defaultValue": "Tiled", @@ -910,8 +910,8 @@ }, { "id": "factor", - "displayName": "Heightmap Scale", - "description": "The total height of the heightmap in local model units.", + "displayName": "Height Map Scale", + "description": "The total height of the height map in local model units.", "type": "Float", "defaultValue": 0.05, "min": 0.0, @@ -972,7 +972,7 @@ { "id": "showClipping", "displayName": "Show Clipping", - "description": "Highlight areas where the heightmap is clipped by the mesh surface.", + "description": "Highlight areas where the height map is clipped by the mesh surface.", "type": "Bool", "defaultValue": false, "connection": { @@ -1009,7 +1009,7 @@ { "id": "influenceMap", "displayName": " Influence Map", - "description": "Use texture map to control the strength of subsurface scattering", + "description": "Texture for controlling the strength of subsurface scattering", "type": "Image", "connection": { "type": "ShaderInput", @@ -1019,7 +1019,7 @@ { "id": "useInfluenceMap", "displayName": " Use Influence Map", - "description": "Whether to use the texture map as influence mask.", + "description": "Whether to use the influence map.", "type": "Bool", "defaultValue": true }, @@ -1088,7 +1088,7 @@ { "id": "thicknessMap", "displayName": " Thickness Map", - "description": "Use a greyscale texture for per pixel thickness", + "description": "Texture for controlling per pixel thickness", "type": "Image", "connection": { "type": "ShaderInput", @@ -1117,7 +1117,7 @@ { "id": "transmissionTint", "displayName": " Transmission Tint", - "description": "Color of the volume light travelling through", + "description": "Color of the volume light traveling through", "type": "Color", "defaultValue": [ 1.0, 0.8, 0.6 ] }, diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h index 9baf80bae0..c8b406f94b 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h @@ -32,7 +32,7 @@ namespace AZ //! In the source data, properties and UV names are loaded separately. //! However, treating UV names as a special property group can greatly simplify the editor code. //! See MaterialInspector::AddUvNamesGroup() for more details. - static constexpr const char UvGroupName[] = "UvNames"; + static constexpr const char UvGroupName[] = "uvSets"; class MaterialAsset; diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Document/MaterialDocumentRequestBus.h b/Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Document/MaterialDocumentRequestBus.h index c71d500d8c..7f36d3fabc 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Document/MaterialDocumentRequestBus.h +++ b/Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Document/MaterialDocumentRequestBus.h @@ -33,7 +33,7 @@ namespace AZ namespace MaterialEditor { //! UVs are processed in a property group but will be handled differently. - static constexpr const char UvGroupName[] = "UvNames"; + static constexpr const char UvGroupName[] = "uvSets"; class MaterialDocumentRequests : public AZ::EBusTraits diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp index 301fd69025..2242d3af1b 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp @@ -819,10 +819,10 @@ namespace MaterialEditor // is implemented. AtomToolsFramework::DynamicPropertyConfig propertyConfig; propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset; - propertyConfig.m_id = "details.materialType"; + propertyConfig.m_id = "overview.materialType"; propertyConfig.m_nameId = "materialType"; propertyConfig.m_displayName = "Material Type"; - propertyConfig.m_groupName = "Details"; + propertyConfig.m_groupName = "Overview"; propertyConfig.m_description = "The material type defines the layout, properties, default values, shader connections, and other " "data needed to create and edit a derived material."; propertyConfig.m_defaultValue = AZStd::any(materialTypeAsset); @@ -834,10 +834,10 @@ namespace MaterialEditor propertyConfig = {}; propertyConfig.m_dataType = AtomToolsFramework::DynamicPropertyType::Asset; - propertyConfig.m_id = "details.parentMaterial"; + propertyConfig.m_id = "overview.parentMaterial"; propertyConfig.m_nameId = "parentMaterial"; propertyConfig.m_displayName = "Parent Material"; - propertyConfig.m_groupName = "Details"; + propertyConfig.m_groupName = "Overview"; propertyConfig.m_description = "The parent material provides an initial configuration whose properties are inherited and overriden by a derived material."; propertyConfig.m_defaultValue = AZStd::any(parentMaterialAsset); @@ -860,7 +860,7 @@ namespace MaterialEditor propertyConfig.m_id = MaterialPropertyId(UvGroupName, shaderInput).GetCStr(); propertyConfig.m_nameId = shaderInput; propertyConfig.m_displayName = shaderInput; - propertyConfig.m_groupName = "UV Names"; + propertyConfig.m_groupName = "UV Sets"; propertyConfig.m_description = shaderInput; propertyConfig.m_defaultValue = uvName; propertyConfig.m_originalValue = uvName; diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp index 706d365027..560c82be9b 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.cpp @@ -79,8 +79,8 @@ namespace MaterialEditor if (!m_documentId.IsNull() && isOpen) { - // Create the top group for displaying details about the material - AddDetailsGroup(); + // Create the top group for displaying overview info about the material + AddOverviewGroup(); // Create groups for displaying editable UV names AddUvNamesGroup(); // Create groups for displaying editable properties @@ -105,25 +105,25 @@ namespace MaterialEditor return property && AtomToolsFramework::ArePropertyValuesEqual(property->GetValue(), property->GetConfig().m_parentValue); } - void MaterialInspector::AddDetailsGroup() + void MaterialInspector::AddOverviewGroup() { const AZ::RPI::MaterialTypeSourceData* materialTypeSourceData = nullptr; MaterialDocumentRequestBus::EventResult( materialTypeSourceData, m_documentId, &MaterialDocumentRequestBus::Events::GetMaterialTypeSourceData); - const AZStd::string groupNameId = "details"; - const AZStd::string groupDisplayName = "Details"; + const AZStd::string groupNameId = "overview"; + const AZStd::string groupDisplayName = "Overview"; const AZStd::string groupDescription = materialTypeSourceData->m_description; auto& group = m_groups[groupNameId]; AtomToolsFramework::DynamicProperty property; MaterialDocumentRequestBus::EventResult( - property, m_documentId, &MaterialDocumentRequestBus::Events::GetProperty, AZ::Name("details.materialType")); + property, m_documentId, &MaterialDocumentRequestBus::Events::GetProperty, AZ::Name("overview.materialType")); group.m_properties.push_back(property); property = {}; MaterialDocumentRequestBus::EventResult( - property, m_documentId, &MaterialDocumentRequestBus::Events::GetProperty, AZ::Name("details.parentMaterial")); + property, m_documentId, &MaterialDocumentRequestBus::Events::GetProperty, AZ::Name("overview.parentMaterial")); group.m_properties.push_back(property); // Passing in same group as main and comparison instance to enable custom value comparison for highlighting modified properties @@ -139,7 +139,7 @@ namespace MaterialEditor MaterialDocumentRequestBus::EventResult(materialAsset, m_documentId, &MaterialDocumentRequestBus::Events::GetAsset); const AZStd::string groupNameId = UvGroupName; - const AZStd::string groupDisplayName = "UV Names"; + const AZStd::string groupDisplayName = "UV Sets"; const AZStd::string groupDescription = "UV set names in this material, which can be renamed to match those in the model."; auto& group = m_groups[groupNameId]; diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.h b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.h index 4080430ff5..65d095f42f 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.h +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Window/MaterialInspector/MaterialInspector.h @@ -52,7 +52,7 @@ namespace MaterialEditor bool CompareInstanceNodeProperties( const AzToolsFramework::InstanceDataNode* source, const AzToolsFramework::InstanceDataNode* target) const; - void AddDetailsGroup(); + void AddOverviewGroup(); void AddUvNamesGroup(); void AddPropertiesGroup(); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp index 3192900ca4..229606a238 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp @@ -184,7 +184,7 @@ namespace AZ void MaterialPropertyInspector::AddUvNamesGroup() { const AZStd::string groupNameId = AZ::RPI::UvGroupName; - const AZStd::string groupDisplayName = "UV Names"; + const AZStd::string groupDisplayName = "UV Sets"; const AZStd::string groupDescription = "UV set names in this material, which can be renamed to match those in the model."; auto& group = m_groups[groupNameId]; From fe8803291a759db566cfaa09a4ad64454dc50583 Mon Sep 17 00:00:00 2001 From: sconel Date: Fri, 28 May 2021 13:53:09 -0700 Subject: [PATCH 22/53] Fix for referencing now deprecated AZ::Transform constructor --- .../Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp index b93844b989..21d1fc5264 100644 --- a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp +++ b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp @@ -118,7 +118,7 @@ namespace ScriptCanvas::Nodeables::Spawning AZ::Vector3 rotationCopy = rotation; AZ::Quaternion rotationQuat = AZ::Quaternion::CreateFromEulerAnglesDegrees(rotationCopy); - entityTransform->SetWorldTM(AZ::Transform(translation, rotationQuat, AZ::Vector3(scale, scale, scale))); + entityTransform->SetWorldTM(AZ::Transform(translation, rotationQuat, scale)); } }; From 0495d26d72284dc95d1d51363ca603eef8311213 Mon Sep 17 00:00:00 2001 From: AMZN-AlexOteiza <82234181+AMZN-AlexOteiza@users.noreply.github.com> Date: Fri, 28 May 2021 22:11:15 +0100 Subject: [PATCH 23/53] Added template for creation of default material library (#1040) --- .../TemplateMaterialLibrary.physmaterial | 158 ++++++++++++++++++ .../Components/EditorSystemComponent.cpp | 83 +++++++-- 2 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 Gems/PhysX/Assets/PhysX/TemplateMaterialLibrary.physmaterial diff --git a/Gems/PhysX/Assets/PhysX/TemplateMaterialLibrary.physmaterial b/Gems/PhysX/Assets/PhysX/TemplateMaterialLibrary.physmaterial new file mode 100644 index 0000000000..481cd2fbfa --- /dev/null +++ b/Gems/PhysX/Assets/PhysX/TemplateMaterialLibrary.physmaterial @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gems/PhysX/Code/Editor/Source/Components/EditorSystemComponent.cpp b/Gems/PhysX/Code/Editor/Source/Components/EditorSystemComponent.cpp index c3f0411d58..b28bf6ab4a 100644 --- a/Gems/PhysX/Code/Editor/Source/Components/EditorSystemComponent.cpp +++ b/Gems/PhysX/Code/Editor/Source/Components/EditorSystemComponent.cpp @@ -31,6 +31,36 @@ namespace PhysX { constexpr const char* DefaultAssetFilename = "SurfaceTypeMaterialLibrary"; + constexpr const char* TemplateAssetFilename = "PhysX/TemplateMaterialLibrary"; + + static AZStd::optional> GetMaterialLibraryTemplate() + { + const auto& assetType = AZ::AzTypeInfo::Uuid(); + + AZStd::vector assetTypeExtensions; + AZ::AssetTypeInfoBus::Event(assetType, &AZ::AssetTypeInfo::GetAssetTypeExtensions, assetTypeExtensions); + + if (assetTypeExtensions.size() == 1) + { + // Constructing the path to the library asset + const AZStd::string& assetExtension = assetTypeExtensions[0]; + + // Use the path relative to the asset root to avoid hardcoding full path in the configuration + AZStd::string relativePath = TemplateAssetFilename; + AzFramework::StringFunc::Path::ReplaceExtension(relativePath, assetExtension.c_str()); + + AZ::Data::AssetId assetId; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, relativePath.c_str(), assetType, false /*autoRegisterIfNotFound*/); + + if (assetId.IsValid()) + { + return AZ::Data::AssetManager::Instance().GetAsset(assetId, AZ::Data::AssetLoadBehavior::NoLoad); + } + } + + return AZStd::nullopt; + } static AZStd::optional> CreateMaterialLibrary(const AZStd::string& fullTargetFilePath, const AZStd::string& relativePath) { @@ -41,29 +71,45 @@ namespace PhysX AZ::Data::AssetId assetId; AZ::Data::AssetCatalogRequestBus::BroadcastResult( - assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, relativePath.c_str(), assetType, true); + assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, relativePath.c_str(), assetType, true /*autoRegisterIfNotFound*/); AZ::Data::Asset newAsset = AZ::Data::AssetManager::Instance().GetAsset(assetId, assetType, AZ::Data::AssetLoadBehavior::Default); - if (Physics::MaterialLibraryAsset* materialLibraryAsset = azrtti_cast(newAsset.GetData())) + if (auto* newMaterialLibraryData = azrtti_cast(newAsset.GetData())) { - // check it out in the source control system - AzToolsFramework::SourceControlCommandBus::Broadcast( - &AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, fullTargetFilePath.c_str(), true, - [](bool /*success*/, const AzToolsFramework::SourceControlFileInfo& /*info*/) {}); + if (auto templateLibraryOpt = GetMaterialLibraryTemplate()) + { + if (const auto* templateMaterialLibData = azrtti_cast(templateLibraryOpt->GetData())) + { + templateLibraryOpt->QueueLoad(); + templateLibraryOpt->BlockUntilLoadComplete(); - // Save the material library asset into a file - auto assetHandler = AZ::Data::AssetManager::Instance().GetHandler(assetType); - if (assetHandler->SaveAssetData(newAsset, &fileStream)) - { - return newAsset; - } - else - { - AZ_Error("PhysX", false, - "CreateSurfaceTypeMaterialLibrary: Unable to save Surface Types Material Library Asset to %s", - fullTargetFilePath.c_str()); + // Fill the newly created material library using the template data + for (const auto& materialData : templateMaterialLibData->GetMaterialsData()) + { + newMaterialLibraryData->AddMaterialData(materialData); + } + + // check it out in the source control system + AzToolsFramework::SourceControlCommandBus::Broadcast( + &AzToolsFramework::SourceControlCommandBus::Events::RequestEdit, fullTargetFilePath.c_str(), true /*allowMultiCheckout*/, + [](bool /*success*/, const AzToolsFramework::SourceControlFileInfo& /*info*/) {}); + + // Save the material library asset into a file + auto assetHandler = AZ::Data::AssetManager::Instance().GetHandler(assetType); + if (assetHandler->SaveAssetData(newAsset, &fileStream)) + { + return newAsset; + } + else + { + AZ_Error( + "PhysX", false, + "CreateSurfaceTypeMaterialLibrary: Unable to save Surface Types Material Library Asset to %s", + fullTargetFilePath.c_str()); + } + } } } } @@ -189,7 +235,8 @@ namespace PhysX AzFramework::StringFunc::Path::ReplaceExtension(relativePath, assetExtension.c_str()); // Try to find an already existing material library - AZ::Data::AssetCatalogRequestBus::BroadcastResult(resultAssetId, &AZ::Data::AssetCatalogRequests::GetAssetIdByPath, relativePath.c_str(), azrtti_typeid(), false); + AZ::Data::AssetCatalogRequestBus::BroadcastResult(resultAssetId, + &AZ::Data::AssetCatalogRequests::GetAssetIdByPath, relativePath.c_str(), azrtti_typeid(), false /*autoRegisterIfNotFound*/); if (!resultAssetId.IsValid()) { From 2112da5f85b91533e6f9f3f804f2ccdc9d850c3f Mon Sep 17 00:00:00 2001 From: Chris Santora Date: Fri, 28 May 2021 14:34:07 -0700 Subject: [PATCH 24/53] Reordered material property groups according to design review. - Base Color - Metallic - Roughness - Specular Reflectance F0 - Normal - Occlusion - Emissive - Subsurface - Clear Coat - Displacement - Opacity - UVs - Irradiance - General Settings ATOM-14002 [Material Editor] Revisit user facing organization and layout of material types --- .../Materials/Types/EnhancedPBR.materialtype | 54 +++++++++---------- .../Assets/Materials/Types/Skin.materialtype | 20 +++---- .../Types/StandardMultilayerPBR.materialtype | 30 +++++------ .../Materials/Types/StandardPBR.materialtype | 36 ++++++------- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index 3b5a653f43..79c4ca3cc3 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -13,11 +13,6 @@ "displayName": "Metallic", "description": "Properties for configuring whether the surface is metallic or not." }, - { - "id": "anisotropy", - "displayName": "Anisotropic Material Response", - "description": "How much is this material response anisotropic." - }, { "id": "roughness", "displayName": "Roughness", @@ -28,25 +23,25 @@ "displayName": "Specular Reflectance f0", "description": "The constant f0 represents the specular reflectance at normal incidence (Fresnel 0 Angle). Used to adjust reflectance of non-metal surfaces." }, - { - "id": "clearCoat", - "displayName": "Clear Coat", - "description": "Properties for configuring gloss clear coat" - }, { "id": "normal", "displayName": "Normal", "description": "Properties related to configuring surface normal." }, { - "id": "opacity", - "displayName": "Opacity", - "description": "Properties for configuring the materials transparency." + "id": "detailLayerGroup", + "displayName": "Detail Layer", + "description": "Properties for Fine Details Layer." }, { - "id": "uv", - "displayName": "UVs", - "description": "Properties for configuring UV transforms." + "id": "detailUV", + "displayName": "Detail Layer UV", + "description": "Properties for modifying detail layer UV." + }, + { + "id": "anisotropy", + "displayName": "Anisotropic Material Response", + "description": "How much is this material response anisotropic." }, { "id": "occlusion", @@ -58,25 +53,30 @@ "displayName": "Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, - { - "id": "parallax", - "displayName": "Displacement", - "description": "Properties for parallax effect produced by a height map." - }, { "id": "subsurfaceScattering", "displayName": "Subsurface Scattering", "description": "Properties for configuring subsurface scattering effects." }, { - "id": "detailLayerGroup", - "displayName": "Detail Layer", - "description": "Properties for Fine Details Layer." + "id": "clearCoat", + "displayName": "Clear Coat", + "description": "Properties for configuring gloss clear coat" + }, + { + "id": "parallax", + "displayName": "Displacement", + "description": "Properties for parallax effect produced by a height map." }, { - "id": "detailUV", - "displayName": "Detail Layer UV", - "description": "Properties for modifying detail layer UV." + "id": "opacity", + "displayName": "Opacity", + "description": "Properties for configuring the materials transparency." + }, + { + "id": "uv", + "displayName": "UVs", + "description": "Properties for configuring UV transforms." }, { // Note: this property group is used in the DiffuseGlobalIllumination pass and not by the main forward shader diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index ab36853723..9ead0376bb 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -23,6 +23,16 @@ "displayName": "Normal", "description": "Properties related to configuring surface normal." }, + { + "id": "detailLayerGroup", + "displayName": "Detail Layer", + "description": "Properties for Fine Details Layer." + }, + { + "id": "detailUV", + "displayName": "Detail Layer UV", + "description": "Properties for modifying detail layer UV." + }, { "id": "occlusion", "displayName": "Occlusion", @@ -38,16 +48,6 @@ "displayName": "Wrinkle Layers", "description": "Properties for wrinkle maps to support morph animation, using vertex color blend weights." }, - { - "id": "detailLayerGroup", - "displayName": "Detail Layer", - "description": "Properties for Fine Details Layer." - }, - { - "id": "detailUV", - "displayName": "Detail Layer UV", - "description": "Properties for modifying detail layer UV." - }, { "id": "general", "displayName": "General Settings", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype index 5b6e5f30bc..c07eac3d47 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype @@ -57,11 +57,6 @@ "displayName": "Layer 1: Normal", "description": "Properties related to configuring surface normal." }, - { - "id": "layer1_clearCoat", - "displayName": "Layer 1: Clear Coat", - "description": "Properties for configuring gloss clear coat" - }, { "id": "layer1_occlusion", "displayName": "Layer 1: Occlusion", @@ -72,6 +67,11 @@ "displayName": "Layer 1: Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, + { + "id": "layer1_clearCoat", + "displayName": "Layer 1: Clear Coat", + "description": "Properties for configuring gloss clear coat" + }, { "id": "layer1_parallax", "displayName": "Layer 1: Displacement", @@ -110,11 +110,6 @@ "displayName": "Layer 2: Normal", "description": "Properties related to configuring surface normal." }, - { - "id": "layer2_clearCoat", - "displayName": "Layer 2: Clear Coat", - "description": "Properties for configuring gloss clear coat" - }, { "id": "layer2_occlusion", "displayName": "Layer 2: Occlusion", @@ -125,6 +120,11 @@ "displayName": "Layer 2: Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, + { + "id": "layer2_clearCoat", + "displayName": "Layer 2: Clear Coat", + "description": "Properties for configuring gloss clear coat" + }, { "id": "layer2_parallax", "displayName": "Layer 2: Displacement", @@ -163,11 +163,6 @@ "displayName": "Layer 3: Normal", "description": "Properties related to configuring surface normal." }, - { - "id": "layer3_clearCoat", - "displayName": "Layer 3: Clear Coat", - "description": "Properties for configuring gloss clear coat" - }, { "id": "layer3_occlusion", "displayName": "Layer 3: Occlusion", @@ -178,6 +173,11 @@ "displayName": "Layer 3: Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, + { + "id": "layer3_clearCoat", + "displayName": "Layer 3: Clear Coat", + "description": "Properties for configuring gloss clear coat" + }, { "id": "layer3_parallax", "displayName": "Layer 3: Displacement", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 0904302085..658aaeeee9 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -23,26 +23,11 @@ "displayName": "Specular Reflectance f0", "description": "The constant f0 represents the specular reflectance at normal incidence (Fresnel 0 Angle). Used to adjust reflectance of non-metal surfaces." }, - { - "id": "clearCoat", - "displayName": "Clear Coat", - "description": "Properties for configuring gloss clear coat" - }, { "id": "normal", "displayName": "Normal", "description": "Properties related to configuring surface normal." }, - { - "id": "opacity", - "displayName": "Opacity", - "description": "Properties for configuring the materials transparency." - }, - { - "id": "uv", - "displayName": "UVs", - "description": "Properties for configuring UV transforms." - }, { "id": "occlusion", "displayName": "Occlusion", @@ -53,15 +38,30 @@ "displayName": "Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, + { + "id": "subsurfaceScattering", + "displayName": "Subsurface Scattering", + "description": "Properties for configuring subsurface scattering effects." + }, + { + "id": "clearCoat", + "displayName": "Clear Coat", + "description": "Properties for configuring gloss clear coat" + }, { "id": "parallax", "displayName": "Displacement", "description": "Properties for parallax effect produced by a height map." }, { - "id": "subsurfaceScattering", - "displayName": "Subsurface Scattering", - "description": "Properties for configuring subsurface scattering effects." + "id": "opacity", + "displayName": "Opacity", + "description": "Properties for configuring the materials transparency." + }, + { + "id": "uv", + "displayName": "UVs", + "description": "Properties for configuring UV transforms." }, { // Note: this property group is used in the DiffuseGlobalIllumination pass, it is not read by the StandardPBR shader From f7e03a2f37b96ffb8ebd96994848f573838ea091 Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Fri, 28 May 2021 16:48:24 -0500 Subject: [PATCH 25/53] Updating the README.md to account for the O3DE as an SDK changes (#1041) Moved the registration of the engine to the "Setting up new projects" section. The engine is no longer required to be registered in order to build it. --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e0442adc16..0c59837a62 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,6 @@ If you have the Git credential manager core or other credential helpers installe ``` python\get_python.bat ``` - -1. While still within the repo folder, register the engine with this command: - ``` - scripts\o3de.bat register --this-engine - ``` 1. Configure the source into a solution using this command line, replacing and <3rdParty cache path> to a path you've created: ``` @@ -146,7 +141,11 @@ If you have the Git credential manager core or other credential helpers installe 1. This will compile after some time and binaries will be available in the build path you've specified -### Setting up new projects +### Setting up new projects +1. While still within the repo folder, register the engine with this command: + ``` + scripts\o3de.bat register --this-engine + ``` 1. Setup new projects using the `o3de create-project` command. In the 0.5 branch, the project directory must be a subdirectory in the repo folder. ``` \scripts\o3de.bat create-project --project-path @@ -160,10 +159,10 @@ If you have the Git credential manager core or other credential helpers installe cmake -B -S -G "Visual Studio 16" -DLY_3RDPARTY_PATH=<3rdParty cache path> // For the 0.5 branch, you must build a new Editor for each project: - cmake --build --target Editor --config profile -- /m + cmake --build --target .GameLauncher Editor --config profile -- /m // For all other branches, just build the project: - cmake --build --target --config profile -- /m + cmake --build --target .GameLauncher --config profile -- /m ``` For a tutorial on project configuration, see [Creating Projects Using the Command Line](https://docs.o3de.org/docs/welcome-guide/get-started/project-config/creating-projects-using-cli) in the documentation. From 147f0084a8e4c0f8a88535e00e7bef300888ecdd Mon Sep 17 00:00:00 2001 From: Chris Santora Date: Fri, 28 May 2021 15:47:25 -0700 Subject: [PATCH 26/53] Removed subsurface scattering and transmission features from StandardPBR.materialtype. ATOM-4120 Stabilize Standard PBR Regarding Subsurface and Translucency --- .../Materials/Types/EnhancedPBR.materialtype | 2 +- ...te.lua => EnhancedPBR_SubsurfaceState.lua} | 0 .../Assets/Materials/Types/Skin.materialtype | 2 +- .../Materials/Types/StandardPBR.materialtype | 207 ------------------ .../Materials/Types/StandardPBR_Common.azsli | 19 -- .../Types/StandardPBR_ForwardPass.azsl | 32 +-- .../Features/PBR/Lighting/LightingData.azsli | 8 +- .../PBR/Lighting/StandardLighting.azsli | 1 - .../PBR/Surfaces/StandardSurface.azsli | 2 +- .../atom_feature_common_asset_files.cmake | 2 +- 10 files changed, 15 insertions(+), 260 deletions(-) rename Gems/Atom/Feature/Common/Assets/Materials/Types/{StandardPBR_SubsurfaceState.lua => EnhancedPBR_SubsurfaceState.lua} (100%) diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index 79c4ca3cc3..ff0c4c59da 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -1636,7 +1636,7 @@ { "type": "Lua", "args": { - "file": "StandardPBR_SubsurfaceState.lua" + "file": "EnhancedPBR_SubsurfaceState.lua" } }, { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_SubsurfaceState.lua b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_SubsurfaceState.lua similarity index 100% rename from Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_SubsurfaceState.lua rename to Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_SubsurfaceState.lua diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index 9ead0376bb..dfe2fad60f 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -1080,7 +1080,7 @@ { "type": "Lua", "args": { - "file": "StandardPBR_SubsurfaceState.lua" + "file": "EnhancedPBR_SubsurfaceState.lua" } }, { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 658aaeeee9..2d94f66edf 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -38,11 +38,6 @@ "displayName": "Emissive", "description": "Properties to add light emission, independent of other lights in the scene." }, - { - "id": "subsurfaceScattering", - "displayName": "Subsurface Scattering", - "description": "Properties for configuring subsurface scattering effects." - }, { "id": "clearCoat", "displayName": "Clear Coat", @@ -981,183 +976,6 @@ } } ], - "subsurfaceScattering": [ - { - "id": "enableSubsurfaceScattering", - "displayName": "Subsurface Scattering", - "description": "Enable subsurface scattering feature, this will disable metallic and parallax mapping property due to incompatibility", - "type": "Bool", - "defaultValue": false, - "connection": { - "type": "ShaderOption", - "id": "o_enableSubsurfaceScattering" - } - }, - { - "id": "subsurfaceScatterFactor", - "displayName": " Factor", - "description": "Strength factor for scaling percentage of subsurface scattering effect applied", - "type": "float", - "defaultValue": 1.0, - "min": 0.0, - "max": 1.0, - "connection": { - "type": "ShaderInput", - "id": "m_subsurfaceScatteringFactor" - } - }, - { - "id": "influenceMap", - "displayName": " Influence Map", - "description": "Texture for controlling the strength of subsurface scattering", - "type": "Image", - "connection": { - "type": "ShaderInput", - "id": "m_subsurfaceScatteringInfluenceMap" - } - }, - { - "id": "useInfluenceMap", - "displayName": " Use Influence Map", - "description": "Whether to use the influence map.", - "type": "Bool", - "defaultValue": true - }, - { - "id": "influenceMapUv", - "displayName": " UV", - "description": "Influence map UV set", - "type": "Enum", - "enumIsUv": true, - "defaultValue": "Tiled", - "connection": { - "type": "ShaderInput", - "id": "m_subsurfaceScatteringInfluenceMapUvIndex" - } - }, - { - "id": "scatterColor", - "displayName": " Scatter color", - "description": "Color of volume light traveled through", - "type": "Color", - "defaultValue": [ 1.0, 0.27, 0.13 ] - }, - { - "id": "scatterDistance", - "displayName": " Scatter distance", - "description": "How far light traveled inside the volume", - "type": "float", - "defaultValue": 8, - "min": 0.0, - "softMax": 20.0 - }, - { - "id": "quality", - "displayName": " Quality", - "description": "How much percent of sample will be used for each pixel, more samples improve quality and reduce artifacts, especially when the scatter distance is relatively large, but slow down computation time, 1.0 = full set 200 samples per pixel", - "type": "float", - "defaultValue": 0.4, - "min": 0.2, - "max": 1.0, - "connection": { - "type": "ShaderInput", - "id": "m_subsurfaceScatteringQuality" - } - }, - { - "id": "transmissionMode", - "displayName": "Transmission", - "description": "Algorithm used for calculating transmission", - "type": "Enum", - "enumValues": [ "None", "ThickObject", "ThinObject" ], - "defaultValue": "None", - "connection": { - "type": "ShaderOption", - "id": "o_transmission_mode" - } - }, - { - "id": "thickness", - "displayName": " Thickness", - "description": "Normalized global thickness, the maxima between this value (multiplied by thickness map if enabled) and thickness from shadow map (if applicable) will be used as final thickness of pixel", - "type": "float", - "defaultValue": 0.5, - "min": 0.0, - "max": 1.0 - }, - { - "id": "thicknessMap", - "displayName": " Thickness Map", - "description": "Texture for controlling per pixel thickness", - "type": "Image", - "connection": { - "type": "ShaderInput", - "id": "m_transmissionThicknessMap" - } - }, - { - "id": "useThicknessMap", - "displayName": " Use Thickness Map", - "description": "Whether to use the thickness map", - "type": "Bool", - "defaultValue": true - }, - { - "id": "thicknessMapUv", - "displayName": " UV", - "description": "Thickness map UV set", - "type": "Enum", - "enumIsUv": true, - "defaultValue": "Tiled", - "connection": { - "type": "ShaderInput", - "id": "m_transmissionThicknessMapUvIndex" - } - }, - { - "id": "transmissionTint", - "displayName": " Transmission Tint", - "description": "Color of the volume light traveling through", - "type": "Color", - "defaultValue": [ 1.0, 0.8, 0.6 ] - }, - { - "id": "transmissionPower", - "displayName": " Power", - "description": "How much transmitted light scatter radially ", - "type": "float", - "defaultValue": 6.0, - "min": 0.0, - "softMax": 20.0 - }, - { - "id": "transmissionDistortion", - "displayName": " Distortion", - "description": "How much light direction distorted towards surface normal", - "type": "float", - "defaultValue": 0.1, - "min": 0.0, - "max": 1.0 - }, - { - "id": "transmissionAttenuation", - "displayName": " Attenuation", - "description": "How fast transmitted light fade with thickness", - "type": "float", - "defaultValue": 4.0, - "min": 0.0, - "softMax": 20.0 - }, - { - "id": "transmissionScale", - "displayName": " Scale", - "description": "Strength of transmission", - "type": "float", - "defaultValue": 3.0, - "min": 0.0, - "softMax": 20.0 - } - ], "irradiance": [ // Note: this property group is used in the DiffuseGlobalIllumination pass and not by the main forward shader { @@ -1261,25 +1079,6 @@ "nitMinMax": [0.001, 100000.0] } }, - { - // Preprocess & build parameter set for subsurface scattering and translucency - "type": "HandleSubsurfaceScatteringParameters", - "args": { - "mode": "subsurfaceScattering.transmissionMode", - "scale": "subsurfaceScattering.transmissionScale", - "power": "subsurfaceScattering.transmissionPower", - "distortion": "subsurfaceScattering.transmissionDistortion", - "attenuation": "subsurfaceScattering.transmissionAttenuation", - "tintColor": "subsurfaceScattering.transmissionTint", - "thickness": "subsurfaceScattering.thickness", - "enabled": "subsurfaceScattering.enableSubsurfaceScattering", - "scatterDistanceColor": "subsurfaceScattering.scatterColor", - "scatterDistanceIntensity": "subsurfaceScattering.scatterDistance", - "scatterDistanceShaderInput": "m_scatterDistance", - "parametersShaderInput": "m_transmissionParams", - "tintThickenssShaderInput": "m_transmissionTintThickness" - } - }, { "type": "UseTexture", "args": { @@ -1364,12 +1163,6 @@ "file": "StandardPBR_Roughness.lua" } }, - { - "type": "Lua", - "args": { - "file": "StandardPBR_SubsurfaceState.lua" - } - }, { "type": "Lua", "args": { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_Common.azsli b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_Common.azsli index 87562c3d20..f339642a4d 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_Common.azsli +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_Common.azsli @@ -73,25 +73,6 @@ ShaderResourceGroup MaterialSrg : SRG_PerMaterial MagFilter = Linear; MipFilter = Linear; }; - - // Parameters for subsurface scattering - float m_subsurfaceScatteringFactor; - float m_subsurfaceScatteringQuality; - float3 m_scatterDistance; - Texture2D m_subsurfaceScatteringInfluenceMap; - uint m_subsurfaceScatteringInfluenceMapUvIndex; - - // Parameters for transmission - - // Elements of m_transmissionParams: - // Thick object mode: (attenuation coefficient, power, distortion, scale) - // Thin object mode: (float3 scatter distance, scale) - float4 m_transmissionParams; - - // (float3 TintColor, thickness) - float4 m_transmissionTintThickness; - Texture2D m_transmissionThicknessMap; - uint m_transmissionThicknessMapUvIndex; } // Callback function for ParallaxMapping.azsli diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl index 286b9b23df..b221ee0a33 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl @@ -47,13 +47,6 @@ COMMON_OPTIONS_EMISSIVE() // Alpha #include "MaterialInputs/AlphaInput.azsli" -// Subsurface -#include "MaterialInputs/SubsurfaceInput.azsli" - -// Transmission -#include "MaterialInputs/TransmissionInput.azsli" - - // ---------- Vertex Shader ---------- struct VSInput @@ -113,7 +106,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz }; - if ((o_parallax_feature_enabled && !o_enableSubsurfaceScattering) || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture)) + if (ShouldHandleParallax() || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture)) { PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); } @@ -124,7 +117,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float bool displacementIsClipped = false; - // Parallax mapping's non uniform uv transformations break screen space subsurface scattering, disable it when subsurface scatteirng is enabled if(ShouldHandleParallax()) { @@ -174,12 +166,8 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float // ------- Metallic ------- - float metallic = 0; - if(!o_enableSubsurfaceScattering) // If subsurface scattering is enabled skip texture lookup for metallic, as this quantity won't be used anyway - { - float2 metallicUv = IN.m_uv[MaterialSrg::m_metallicMapUvIndex]; - metallic = GetMetallicInput(MaterialSrg::m_metallicMap, MaterialSrg::m_sampler, metallicUv, MaterialSrg::m_metallicFactor, o_metallic_useTexture); - } + float2 metallicUv = IN.m_uv[MaterialSrg::m_metallicMapUvIndex]; + float metallic = GetMetallicInput(MaterialSrg::m_metallicMap, MaterialSrg::m_sampler, metallicUv, MaterialSrg::m_metallicFactor, o_metallic_useTexture); // ------- Specular ------- @@ -195,11 +183,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float MaterialSrg::m_roughnessLowerBound, MaterialSrg::m_roughnessUpperBound, o_roughness_useTexture); surface.CalculateRoughnessA(); - // ------- Subsurface ------- - - float surfaceScatteringFactor = 0.0f; - surface.transmission.InitializeToZero(); - // ------- Lighting Data ------- LightingData lightingData; @@ -271,7 +254,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float ApplyIBL(surface, lightingData); // Finalize Lighting - lightingData.FinalizeLighting(surface.transmission.tint); + lightingData.FinalizeLighting(); if (o_opacity_mode == OpacityMode::Blended || o_opacity_mode == OpacityMode::TintedTransparent) @@ -312,13 +295,6 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float lightingOutput.m_diffuseColor.rgb += lightingOutput.m_specularColor.rgb; // add specular lightingOutput.m_specularColor.rgb = baseColor * (1.0 - lightingOutput.m_diffuseColor.w); } - else - { - // Pack factor and quality, drawback: because of precision limit of float16 cannot represent exact 1, maximum representable value is 0.9961 - uint factorAndQuality = dot(round(float2(saturate(surfaceScatteringFactor), MaterialSrg::m_subsurfaceScatteringQuality) * 255), float2(256, 1)); - lightingOutput.m_diffuseColor.w = factorAndQuality * (o_enableSubsurfaceScattering ? 1.0 : -1.0); - lightingOutput.m_scatterDistance = MaterialSrg::m_scatterDistance; - } return lightingOutput; } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/LightingData.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/LightingData.azsli index 37cae0acb1..e37aa55c09 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/LightingData.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/LightingData.azsli @@ -54,6 +54,7 @@ class LightingData void Init(float3 positionWS, float3 normal, float roughnessLinear); void CalculateMultiscatterCompensation(float3 specularF0, bool enabled); + void FinalizeLighting(); void FinalizeLighting(float3 transmissionTint); }; @@ -80,10 +81,15 @@ void LightingData::CalculateMultiscatterCompensation(float3 specularF0, bool ena multiScatterCompensation = GetMultiScatterCompensation(specularF0, brdf, enabled); } -void LightingData::FinalizeLighting(float3 transmissionTint) +void LightingData::FinalizeLighting() { specularLighting *= specularOcclusion; specularLighting += emissiveLighting; +} + +void LightingData::FinalizeLighting(float3 transmissionTint) +{ + FinalizeLighting(); // Transmitted light if(o_transmission_mode != TransmissionMode::None) diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/StandardLighting.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/StandardLighting.azsli index 45aabeede4..f690163a58 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/StandardLighting.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lighting/StandardLighting.azsli @@ -75,7 +75,6 @@ struct PbrLightingOutput float4 m_albedo; float4 m_specularF0; float4 m_normal; - float3 m_scatterDistance; }; diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Surfaces/StandardSurface.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Surfaces/StandardSurface.azsli index 1a74a68e96..bc4e41d2c2 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Surfaces/StandardSurface.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Surfaces/StandardSurface.azsli @@ -20,7 +20,7 @@ class Surface { ClearCoatSurfaceData clearCoat; - TransmissionSurfaceData transmission; + TransmissionSurfaceData transmission; // This is not actually used for Standard PBR, but must be present for common lighting code to compile // ------- BasePbrSurfaceData ------- diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake index 841cdf67c8..359c0b9b20 100644 --- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake +++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake @@ -23,6 +23,7 @@ set(FILES Materials/Types/EnhancedPBR_ForwardPass_EDS.shader Materials/Types/EnhancedPBR_Shadowmap_WithPS.azsl Materials/Types/EnhancedPBR_Shadowmap_WithPS.shader + Materials/Types/EnhancedPBR_SubsurfaceState.lua Materials/Types/Skin.azsl Materials/Types/Skin.materialtype Materials/Types/Skin.shader @@ -61,7 +62,6 @@ set(FILES Materials/Types/StandardPBR_ShaderEnable.lua Materials/Types/StandardPBR_Shadowmap_WithPS.azsl Materials/Types/StandardPBR_Shadowmap_WithPS.shader - Materials/Types/StandardPBR_SubsurfaceState.lua Materials/Types/MaterialInputs/AlphaInput.azsli Materials/Types/MaterialInputs/BaseColorInput.azsli Materials/Types/MaterialInputs/ClearCoatInput.azsli From 75cb293b2a0309335d9b7aea78ca9f0cd61fa41a Mon Sep 17 00:00:00 2001 From: hershey5045 <43485729+hershey5045@users.noreply.github.com> Date: Fri, 28 May 2021 16:40:44 -0700 Subject: [PATCH 27/53] Png fix for vulkan rhi (#962) * Add supported formats for pngs in frame capture system. Add conversion logic from bgra to rgba. --- .../Source/FrameCaptureSystemComponent.cpp | 65 ++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp index 90499be8b8..f8c1258fc0 100644 --- a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include #include #include @@ -55,6 +57,43 @@ namespace AZ FrameCaptureOutputResult PngFrameCaptureOutput( const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) { + AZStd::shared_ptr> buffer = readbackResult.m_dataBuffer; + + // convert bgra to rgba by swapping channels + const int numChannels = AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format); + if (readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM) + { + buffer = AZStd::make_shared>(readbackResult.m_dataBuffer->size()); + AZStd::copy(readbackResult.m_dataBuffer->begin(), readbackResult.m_dataBuffer->end(), buffer->begin()); + + AZ::JobCompletion jobCompletion; + const int numThreads = 8; + const int numPixelsPerThread = buffer->size() / numChannels / numThreads; + for (int i = 0; i < numThreads; ++i) + { + int startPixel = i * numPixelsPerThread; + + AZ::Job* job = AZ::CreateJobFunction( + [&, startPixel, numPixelsPerThread]() + { + for (int pixelOffset = 0; pixelOffset < numPixelsPerThread; ++pixelOffset) + { + if (startPixel * numChannels + numChannels < buffer->size()) + { + AZStd::swap( + buffer->data()[(startPixel + pixelOffset) * numChannels], + buffer->data()[(startPixel + pixelOffset) * numChannels + 2] + ); + } + } + }, true, nullptr); + + job->SetDependent(&jobCompletion); + job->Start(); + } + jobCompletion.StartAndWaitForCompletion(); + } + using namespace OIIO; AZStd::unique_ptr out = ImageOutput::create(outputFilePath.c_str()); if (out) @@ -62,13 +101,13 @@ namespace AZ ImageSpec spec( readbackResult.m_imageDescriptor.m_size.m_width, readbackResult.m_imageDescriptor.m_size.m_height, - AZ::RHI::GetFormatComponentCount(readbackResult.m_imageDescriptor.m_format) + numChannels ); spec.attribute("png:compressionLevel", r_pngCompressionLevel); if (out->open(outputFilePath.c_str(), spec)) { - out->write_image(TypeDesc::UINT8, readbackResult.m_dataBuffer->data()); + out->write_image(TypeDesc::UINT8, buffer->data()); out->close(); return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt}; } @@ -460,13 +499,23 @@ namespace AZ #if defined(OPEN_IMAGE_IO_ENABLED) else if (extension == "png") { - AZStd::string folderPath; - AzFramework::StringFunc::Path::GetFolderPath(m_outputFilePath.c_str(), folderPath); - AZ::IO::SystemFile::CreateDir(folderPath.c_str()); + if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM || + readbackResult.m_imageDescriptor.m_format == RHI::Format::B8G8R8A8_UNORM) + { + AZStd::string folderPath; + AzFramework::StringFunc::Path::GetFolderPath(m_outputFilePath.c_str(), folderPath); + AZ::IO::SystemFile::CreateDir(folderPath.c_str()); - const auto frameCaptureResult = PngFrameCaptureOutput(m_outputFilePath, readbackResult); - m_result = frameCaptureResult.m_result; - m_latestCaptureInfo = frameCaptureResult.m_errorMessage.value_or(""); + const auto frameCaptureResult = PngFrameCaptureOutput(m_outputFilePath, readbackResult); + m_result = frameCaptureResult.m_result; + m_latestCaptureInfo = frameCaptureResult.m_errorMessage.value_or(""); + } + else + { + m_latestCaptureInfo = AZStd::string::format( + "Can't save image with format %s to a png file", RHI::ToString(readbackResult.m_imageDescriptor.m_format)); + m_result = FrameCaptureResult::UnsupportedFormat; + } } #endif else From 50a9e94eca9d5cc68567c30cfe3142b50dee5e58 Mon Sep 17 00:00:00 2001 From: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> Date: Fri, 28 May 2021 18:28:29 -0700 Subject: [PATCH 28/53] Fix old method call (#1049) --- .../Code/Source/RayTracing/RayTracingFeatureProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp index f51fa4b81b..10c7c2d378 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp @@ -234,7 +234,7 @@ namespace AZ { AZ::Transform meshTransform = transformFeatureProcessor->GetTransformForId(TransformServiceFeatureProcessorInterface::ObjectId(mesh.first)); AZ::Transform noScaleTransform = meshTransform; - noScaleTransform.ExtractScale(); + noScaleTransform.ExtractUniformScale(); AZ::Matrix3x3 rotationMatrix = Matrix3x3::CreateFromTransform(noScaleTransform); rotationMatrix = rotationMatrix.GetInverseFull().GetTranspose(); From 74ec7a362b25cd55d9429142188892e1cbe937ba Mon Sep 17 00:00:00 2001 From: Steve Pham <82231385+spham-amzn@users.noreply.github.com> Date: Fri, 28 May 2021 23:24:51 -0700 Subject: [PATCH 29/53] Update Android Project Generation to support AGP 4.2.0, Cmake 3.20, and newer versions of NDK&SDK - build.gradle format updates for newer Android Gradle Plugin - Remove hard coded Android Gradle Plugin version 3.6.4 to be passed in from command args - Set Android Gradle Plugin min version 4.2.0 in order to support Min CMake version 3.20 - Add ability to use the android sdk to install missing components if needed rather than doing it externally - Removed argument to pass in the NDK folder to use the android, use the android-sdk instead. Can request specific NDK versions if possible - Android Gradle Plugin has dependencies by version and is being managed - More defaults based on tools on path, agp version made possible so they are no longer needed in the command args --- .../Android/ProjectBuilder/build.gradle.in | 4 +- .../ProjectBuilder/local.properties.in | 1 - .../ProjectBuilder/root.build.gradle.in | 7 +- .../Tools/Platform/Android/android_support.py | 615 +++++++++--------- .../Android/generate_android_project.py | 164 +++-- .../unit_test_generate_android_project.py | 114 ---- cmake/Tools/common.py | 37 +- .../build/Platform/Android/build_config.json | 6 +- .../build/Platform/Android/gradle_windows.cmd | 39 +- scripts/build/Platform/Android/pipeline.json | 4 +- 10 files changed, 490 insertions(+), 501 deletions(-) diff --git a/Code/Tools/Android/ProjectBuilder/build.gradle.in b/Code/Tools/Android/ProjectBuilder/build.gradle.in index 66f58294ab..5980984516 100644 --- a/Code/Tools/Android/ProjectBuilder/build.gradle.in +++ b/Code/Tools/Android/ProjectBuilder/build.gradle.in @@ -15,14 +15,14 @@ android { ${SIGNING_CONFIGS} compileSdkVersion sdkVer buildToolsVersion buildToolsVer - + ndkVersion ndkPlatformVer lintOptions { abortOnError false checkReleaseBuilds false } defaultConfig { - minSdkVersion ndkPlatformVer + minSdkVersion minSdkVer targetSdkVersion sdkVer ${NATIVE_CMAKE_SECTION_DEFAULT_CONFIG} } diff --git a/Code/Tools/Android/ProjectBuilder/local.properties.in b/Code/Tools/Android/ProjectBuilder/local.properties.in index 559ea67bcb..4e82cb2940 100644 --- a/Code/Tools/Android/ProjectBuilder/local.properties.in +++ b/Code/Tools/Android/ProjectBuilder/local.properties.in @@ -16,6 +16,5 @@ # For customization when using a Version Control System, please read the # header note. # ${GENERATION_TIMESTAMP} -ndk.dir=${ANDROID_NDK_PATH} sdk.dir=${ANDROID_SDK_PATH} ${CMAKE_DIR_LINE} diff --git a/Code/Tools/Android/ProjectBuilder/root.build.gradle.in b/Code/Tools/Android/ProjectBuilder/root.build.gradle.in index 782a1f26b5..dfce99c3c7 100644 --- a/Code/Tools/Android/ProjectBuilder/root.build.gradle.in +++ b/Code/Tools/Android/ProjectBuilder/root.build.gradle.in @@ -12,10 +12,9 @@ buildscript { repositories { google() jcenter() - } dependencies { - classpath 'com.android.tools.build:gradle:3.6.4' + classpath 'com.android.tools.build:gradle:${ANDROID_GRADLE_PLUGIN_VERSION}' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -26,14 +25,14 @@ allprojects { repositories { google() jcenter() - } } subprojects { ext { + minSdkVer = ${MIN_SDK_VER} sdkVer = ${SDK_VER} - ndkPlatformVer = ${NDK_PLATFORM_VER} + ndkPlatformVer = '${NDK_VERSION}' buildToolsVer = '${SDK_BUILD_TOOL_VER}' lyEngineRoot = '${LY_ENGINE_ROOT}' } diff --git a/cmake/Tools/Platform/Android/android_support.py b/cmake/Tools/Platform/Android/android_support.py index 6443077457..75e0cd2970 100755 --- a/cmake/Tools/Platform/Android/android_support.py +++ b/cmake/Tools/Platform/Android/android_support.py @@ -10,7 +10,9 @@ # import imghdr +import configparser import datetime +import fnmatch import logging import os import json @@ -33,6 +35,13 @@ if ROOT_DEV_PATH not in sys.path: from cmake.Tools import common +ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP = { + '4.2.0': {'min_gradle_version': '6.7.1', + 'sdk_build': '30.0.2', + 'default_ndk': '21.4.7075529', + 'min_cmake_version': '3.20'} +} + APP_NAME = 'app' ANDROID_MANIFEST_FILE = 'AndroidManifest.xml' ANDROID_LIBRARIES_JSON_FILE = 'android_libraries.json' @@ -86,83 +95,93 @@ PYTHON_SCRIPT = 'python.cmd' if platform.system() == 'Windows' else 'python.sh' ANDROID_LAUNCHER_NAME_PATTERN = "{project_name}.GameLauncher" + class AndroidProjectManifestEnvironment(object): """ - This class manages the environment for the AndroidManifiest.xml template file, based on project settings and environments + This class manages the environment for the AndroidManifest.xml template file, based on project settings and environments that were passed in or calculated from the command line arguments. """ - def __init__(self, engine_root, project_path, android_sdk_version_number, android_ndk_platform_number, is_test:bool): + def __init__(self, engine_root, project_path, android_sdk_version_number, is_test:bool): """ Initialize the object with the project specific parameters and values for the game project :param engine_root: The path where the engine is located :param project_path: The path were the project is located :param android_sdk_version_number: The android SDK platform version - :param android_ndk_platform_number: The android NDK platform version :param is_test: Indicates if theAzTestRunner application should be run """ - if is_test: - # The AzTestRunner project.json is located under {engine_root}/Code/Tools/AzTestRunner/Platform/Android/android_project.json - project_properties_path = engine_root / 'Code' / 'Tools' / 'AzTestRunner' / 'Platform' / 'Android' / 'android_project.json' - else: - # The project.json file is located under the game name folder - project_properties_path = project_path / 'project.json' - # Read and parse the project.json file into a dictionary to process the specific attributes needed for the manifest template - project_properties_content = project_properties_path.resolve(strict=True)\ - .read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING, - errors=common.ENCODING_ERROR_HANDLINGS) - self.project_path = project_path + try: + if is_test: + # The AzTestRunner project.json is located under {engine_root}/Code/Tools/AzTestRunner/Platform/Android/android_project.json + project_properties_path = engine_root / 'Code' / 'Tools' / 'AzTestRunner' / 'Platform' / 'Android' / 'android_project.json' + assert project_properties_path.is_file(), f'Missing required android settings file {project_properties_path.resolve()}' + project_properties_content = project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING, + errors=common.ENCODING_ERROR_HANDLINGS) + project_json = json.loads(project_properties_content) - # Extract the key attributes we need to process and build up our environment table - project_json = json.loads(project_properties_content) + android_settings = project_json['android_settings'] - project_name = project_json.get('project_name') - if not project_name: - raise common.LmbrCmdError(f"Missing required 'project_name' from project.json for project at '{str(project_path)}'") - product_name = project_json.get('product_name', project_name) + else: + # O3DE projects have both a project.json and an android_project.json files (unless its internal) + project_properties_path = project_path / 'project.json' + assert project_properties_path.is_file(), f'Missing required project settings file {project_properties_path.resolve()}' + project_properties_content = project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING, + errors=common.ENCODING_ERROR_HANDLINGS) + project_json = json.loads(project_properties_content) - game_project_android_settings = project_json['android_settings'] + android_project_properties_path = project_path / 'Platform' / 'Android' / 'android_project.json' + if android_project_properties_path.is_file(): + android_project_properties_content = android_project_properties_path.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING, + errors=common.ENCODING_ERROR_HANDLINGS) + android_project_json = json.loads(android_project_properties_content) + android_settings = android_project_json['android_settings'] + else: + android_settings = project_json['android_settings'] - package_name = game_project_android_settings["package_name"] + self.project_path = project_path - package_path = package_name.replace('.', '/') + project_name = project_json['project_name'] + product_name = project_json.get('product_name', project_name) + package_name = android_settings["package_name"] + package_path = package_name.replace('.', '/') - project_activity = f'{TEST_RUNNER_PROJECT}Activity' if is_test else f'{project_name}Activity' + project_activity = f'{TEST_RUNNER_PROJECT}Activity' if is_test else f'{project_name}Activity' - # Multiview options require special processing - multi_window_options = AndroidProjectManifestEnvironment.process_android_multi_window_options(game_project_android_settings) + # Multiview options require special processing + multi_window_options = AndroidProjectManifestEnvironment.process_android_multi_window_options(android_settings) - self.internal_dict = { - 'ANDROID_PACKAGE': package_name, - 'ANDROID_PACKAGE_PATH': package_path, - 'ANDROID_VERSION_NUMBER': game_project_android_settings["version_number"], - "ANDROID_VERSION_NAME": game_project_android_settings["version_name"], - "ANDROID_SCREEN_ORIENTATION": game_project_android_settings["orientation"], - 'ANDROID_APP_NAME': TEST_RUNNER_PROJECT if is_test else product_name, # external facing name - 'ANDROID_PROJECT_NAME': TEST_RUNNER_PROJECT if is_test else project_name, # internal facing name - 'ANDROID_PROJECT_ACTIVITY': project_activity, - 'ANDROID_LAUNCHER_NAME': TEST_RUNNER_PROJECT if is_test else ANDROID_LAUNCHER_NAME_PATTERN.format(project_name=project_name), - 'ANDROID_CONFIG_CHANGES': multi_window_options['ANDROID_CONFIG_CHANGES'], - 'ANDROID_APP_PUBLIC_KEY': game_project_android_settings.get('app_public_key', 'NoKey'), - 'ANDROID_APP_OBFUSCATOR_SALT': game_project_android_settings.get('app_obfuscator_salt', ''), - 'ANDROID_USE_MAIN_OBB': game_project_android_settings.get('use_main_obb', 'false'), - 'ANDROID_USE_PATCH_OBB': game_project_android_settings.get('use_patch_obb', 'false'), - 'ANDROID_ENABLE_KEEP_SCREEN_ON': game_project_android_settings.get('enable_keep_screen_on', 'false'), - 'ANDROID_DISABLE_IMMERSIVE_MODE': game_project_android_settings.get('disable_immersive_mode', 'false'), - 'ANDROID_MIN_SDK_VERSION': android_ndk_platform_number, - 'ANDROID_TARGET_SDK_VERSION': android_sdk_version_number, - 'ICONS': game_project_android_settings.get('icons', None), - 'SPLASH_SCREEN': game_project_android_settings.get('splash_screen', None), + self.internal_dict = { + 'ANDROID_PACKAGE': package_name, + 'ANDROID_PACKAGE_PATH': package_path, + 'ANDROID_VERSION_NUMBER': android_settings["version_number"], + "ANDROID_VERSION_NAME": android_settings["version_name"], + "ANDROID_SCREEN_ORIENTATION": android_settings["orientation"], + 'ANDROID_APP_NAME': TEST_RUNNER_PROJECT if is_test else product_name, # external facing name + 'ANDROID_PROJECT_NAME': TEST_RUNNER_PROJECT if is_test else project_name, # internal facing name + 'ANDROID_PROJECT_ACTIVITY': project_activity, + 'ANDROID_LAUNCHER_NAME': TEST_RUNNER_PROJECT if is_test else ANDROID_LAUNCHER_NAME_PATTERN.format(project_name=project_name), + 'ANDROID_CONFIG_CHANGES': multi_window_options['ANDROID_CONFIG_CHANGES'], + 'ANDROID_APP_PUBLIC_KEY': android_settings.get('app_public_key', 'NoKey'), + 'ANDROID_APP_OBFUSCATOR_SALT': android_settings.get('app_obfuscator_salt', ''), + 'ANDROID_USE_MAIN_OBB': android_settings.get('use_main_obb', 'false'), + 'ANDROID_USE_PATCH_OBB': android_settings.get('use_patch_obb', 'false'), + 'ANDROID_ENABLE_KEEP_SCREEN_ON': android_settings.get('enable_keep_screen_on', 'false'), + 'ANDROID_DISABLE_IMMERSIVE_MODE': android_settings.get('disable_immersive_mode', 'false'), + 'ANDROID_TARGET_SDK_VERSION': android_sdk_version_number, + 'ICONS': android_settings.get('icons', None), + 'SPLASH_SCREEN': android_settings.get('splash_screen', None), - 'ANDROID_MULTI_WINDOW': multi_window_options['ANDROID_MULTI_WINDOW'], - 'ANDROID_MULTI_WINDOW_PROPERTIES': multi_window_options['ANDROID_MULTI_WINDOW_PROPERTIES'], + 'ANDROID_MULTI_WINDOW': multi_window_options['ANDROID_MULTI_WINDOW'], + 'ANDROID_MULTI_WINDOW_PROPERTIES': multi_window_options['ANDROID_MULTI_WINDOW_PROPERTIES'], - 'SAMSUNG_DEX_KEEP_ALIVE': multi_window_options['SAMSUNG_DEX_KEEP_ALIVE'], - 'SAMSUNG_DEX_LAUNCH_WIDTH': multi_window_options['SAMSUNG_DEX_LAUNCH_WIDTH'], - 'SAMSUNG_DEX_LAUNCH_HEIGHT': multi_window_options['SAMSUNG_DEX_LAUNCH_HEIGHT'] - } + 'SAMSUNG_DEX_KEEP_ALIVE': multi_window_options['SAMSUNG_DEX_KEEP_ALIVE'], + 'SAMSUNG_DEX_LAUNCH_WIDTH': multi_window_options['SAMSUNG_DEX_LAUNCH_WIDTH'], + 'SAMSUNG_DEX_LAUNCH_HEIGHT': multi_window_options['SAMSUNG_DEX_LAUNCH_HEIGHT'] + } + except KeyError as e: + raise common.LmbrCmdError(f"Missing key from android project settings for project at {project_path}:'{e}' ") def __getitem__(self, item): return self.internal_dict.get(item) @@ -306,6 +325,7 @@ asset_deploy_type={asset_type} android_sdk_path={android_sdk_path} embed_assets_in_apk={embed_assets_in_apk} is_unit_test={is_unit_test} +android_gradle_plugin={android_gradle_plugin_version} """ NATIVE_CMAKE_SECTION_ANDROID_FORMAT = """ @@ -425,26 +445,28 @@ class AndroidProjectGenerator(object): Class the manages the process to generate an android project folder in order to build with gradle/android studio """ - def __init__(self, engine_root, build_dir, android_ndk_path, android_sdk_path, android_sdk_version, android_ndk_platform, - project_path, third_party_path, cmake_version, override_cmake_path, override_gradle_path, override_ninja_path, - android_sdk_build_tool_version, include_assets_in_apk, asset_mode, asset_type, signing_config, is_test_project=False, + def __init__(self, engine_root, build_dir, android_sdk_path, build_tool, android_sdk_platform, android_native_api_level, android_ndk, + project_path, third_party_path, cmake_version, override_cmake_path, override_gradle_path, gradle_version, gradle_plugin_version, + override_ninja_path, include_assets_in_apk, asset_mode, asset_type, signing_config, is_test_project=False, overwrite_existing=True): """ Initialize the object with all the required parameters needed to create an Android Project. The parameters should be verified before initializing this object - + :param engine_root: The engine root that contains the engine :param build_dir: The target folder under the where the android project folder will be created - :param android_ndk_path: The path to the ANDROID_NDK used for building the native android code :param android_sdk_path: The path to the ANDROID_SDK used for building the android java code - :param android_sdk_version: The android platform version number to use for the Android SDK related builds - :param android_ndk_platform: The android platform version number to use for the Android NDK related builds + :param build_tool: The android SDK build-tool version. + :param android_sdk_platform: The android sdk platform version number to use for the Android SDK related builds + :param android_native_api_level:The android native API level (ANDROID_NATIVE_API_LEVEL) to set + :param android_ndk: The android ndk version number to use for the native builds :param project_path: The path to the project :param third_party_path: The required path to the lumberyard 3rd party path :param cmake_version: The version number of cmake that will be used by gradle :param override_cmake_path: The override path to cmake if it does not exists in the system path :param override_gradle_path: The override path to gradle if it does not exists in the system path + :param gradle_version: The detected version of gradle being used + :param gradle_plugin_version: The android gradle plugin version :param override_ninja_path: The override path to ninja if it does not exists in the system path - :param android_sdk_build_tool_version: The preferred android SDK build-tool version. Will default to the first one detected in the android sdk path :param include_assets_in_apk: :param asset_mode: :param asset_type: @@ -458,17 +480,16 @@ class AndroidProjectGenerator(object): self.build_dir = build_dir - self.android_ndk_path = android_ndk_path - self.android_sdk_path = android_sdk_path self.android_project_builder_path = self.engine_root / 'Code/Tools/Android/ProjectBuilder' - self.android_sdk_version = android_sdk_version + self.android_sdk_platform = android_sdk_platform + self.android_sdk_build_tool_version = build_tool.version - self.android_sdk_build_tool_version = android_sdk_build_tool_version - - self.android_ndk_platform = android_ndk_platform + self.android_ndk = android_ndk + self.android_ndk_version = android_ndk.version + self.android_native_api_level = android_native_api_level self.project_path = project_path @@ -480,6 +501,10 @@ class AndroidProjectGenerator(object): self.override_gradle_path = override_gradle_path + self.gradle_version = gradle_version + + self.gradle_plugin_version = gradle_plugin_version + self.override_ninja_path = override_ninja_path self.include_assets_in_apk = include_assets_in_apk @@ -511,8 +536,10 @@ class AndroidProjectGenerator(object): project_names.extend(self.create_lumberyard_app(project_names)) root_gradle_env = { - 'SDK_VER': self.android_sdk_version, - 'NDK_PLATFORM_VER': self.android_ndk_platform, + 'ANDROID_GRADLE_PLUGIN_VERSION': str(self.gradle_plugin_version), + 'SDK_VER': self.android_sdk_platform, + 'MIN_SDK_VER': self.android_sdk_platform, + 'NDK_VERSION': self.android_ndk_version, 'SDK_BUILD_TOOL_VER': self.android_sdk_build_tool_version, 'LY_ENGINE_ROOT': common.normalize_path_for_settings(self.engine_root) } @@ -557,7 +584,7 @@ class AndroidProjectGenerator(object): if self.override_gradle_path: gradle_wrapper_cmd = [self.override_gradle_path] else: - gradle_wrapper_cmd = ['gradle.bat' if platform.system() == 'Windows' else 'gradle'] + gradle_wrapper_cmd = ['gradle'] gradle_wrapper_cmd.extend(['wrapper', '-p', str(self.build_dir.resolve())]) @@ -580,7 +607,8 @@ class AndroidProjectGenerator(object): asset_type='', android_sdk_path=str(self.android_sdk_path), embed_assets_in_apk=True, - is_unit_test=True) + is_unit_test=True, + android_gradle_plugin_version=self.gradle_plugin_version) else: platform_settings_content = PLATFORM_SETTINGS_FORMAT.format(generation_timestamp=str(datetime.datetime.now().strftime("%c")), platform='android', @@ -589,16 +617,28 @@ class AndroidProjectGenerator(object): asset_type=self.asset_type, android_sdk_path=str(self.android_sdk_path), embed_assets_in_apk=str(self.include_assets_in_apk), - is_unit_test=False) + is_unit_test=False, + android_gradle_plugin_version=self.gradle_plugin_version) platform_settings_file = self.build_dir / 'platform.settings' + + # Check if there already exists the build folder and a 'platform.settings' file. If there is an android gradle + # plugin version set and it is different than the one configured here, we will always overwrite it since + # there could be significant differences from one plug-in to the next + if platform_settings_file.is_file(): + config = configparser.ConfigParser() + config.read([str(platform_settings_file.resolve(strict=True))]) + if config.has_option('android', 'android_gradle_plugin'): + exist_agp_version = config.get('android', 'android_gradle_plugin') + if exist_agp_version != self.gradle_plugin_version: + self.overwrite_existing = True + platform_settings_file.open('w').write(platform_settings_content) def create_default_local_properties(self): """ Create the default 'local.properties' file in the build folder """ - template_android_ndk_path = common.normalize_path_for_settings(self.android_ndk_path, True) template_android_sdk_path = common.normalize_path_for_settings(self.android_sdk_path, True) if self.override_cmake_path: # The cmake dir references the base cmake folder, not the executable path itself, so resolve to the base folder @@ -608,7 +648,6 @@ class AndroidProjectGenerator(object): local_properties_env = { "GENERATION_TIMESTAMP": str(datetime.datetime.now().strftime("%c")), - "ANDROID_NDK_PATH": template_android_ndk_path, "ANDROID_SDK_PATH": template_android_sdk_path, "CMAKE_DIR_LINE": f'cmake.dir={template_cmake_path}' if template_cmake_path else '' } @@ -626,8 +665,7 @@ class AndroidProjectGenerator(object): # before we can process it. android_libraries_substitution_table = { "ANDROID_SDK_HOME": common.normalize_path_for_settings(self.android_sdk_path, False), - "ANDROID_NDK_HOME": common.normalize_path_for_settings(self.android_ndk_path, False), - "ANDROID_SDK_VERSION": "android-".format(self.android_sdk_version) + "ANDROID_SDK_VERSION": f"android-{self.android_sdk_platform}" } android_libraries_template_json_path = self.android_project_builder_path / ANDROID_LIBRARIES_JSON_FILE @@ -717,7 +755,7 @@ class AndroidProjectGenerator(object): template_engine_root = common.normalize_path_for_settings(self.engine_root) template_third_party_path = common.normalize_path_for_settings(self.third_party_path) - template_ndk_path = common.normalize_path_for_settings(self.android_ndk_path) + template_ndk_path = common.normalize_path_for_settings(os.path.join(self.android_sdk_path, self.android_ndk.location)) gradle_build_env = dict() @@ -733,7 +771,6 @@ class AndroidProjectGenerator(object): gradle_build_env['OVERRIDE_JAVA_SOURCESET'] = OVERRIDE_JAVA_SOURCESET_STR.format(absolute_azandroid_path=absolute_azandroid_path) - gradle_build_env['OPTIONAL_JNI_SRC_LIB_SET'] = ', "outputs/native-lib"' for native_config in BUILD_CONFIGURATIONS: @@ -755,7 +792,7 @@ class AndroidProjectGenerator(object): cmake_argument_list.append('"-DLY_TEST_PROJECT=1"') cmake_argument_list.extend([ - f'"-DANDROID_NATIVE_API_LEVEL={self.android_ndk_platform}"', + f'"-DANDROID_NATIVE_API_LEVEL={self.android_native_api_level}"', f'"-DLY_NDK_DIR={template_ndk_path}"', '"-DANDROID_STL=c++_shared"', '"-Wno-deprecated"', @@ -835,8 +872,7 @@ class AndroidProjectGenerator(object): dest_src_main_path.mkdir(parents=True) az_android_package_env = AndroidProjectManifestEnvironment(engine_root=self.engine_root, project_path=self.project_path, - android_sdk_version_number=self.android_sdk_version, - android_ndk_platform_number=self.android_ndk_platform, + android_sdk_version_number=self.android_sdk_platform, is_test=self.is_test_project) self.create_file_from_project_template(src_template_file=ANDROID_MANIFEST_FILE, template_env=az_android_package_env, @@ -1304,218 +1340,7 @@ class AndroidProjectGenerator(object): self.new = new -ANDROID_PLATFORM_PATTERN = re.compile(r'([\w\d]*-)?(\d+\d*)') # Regex to handle android platform naming for both SDKs and NDKs - - -def validate_android_platform_input(input_android_platform, platform_variable_type, min_version, max_version): - """ - Helper tool to support android platform number inputs and perform min/max version validation - - :param input_android_platform: The inpuit argument to evaluate - :param platform_variable_type: The type of platform version to validate (android sdk / android ndk) - :param min_version: The minimum version to validate against - :param max_version: The maximum version to validate against - :return: The int version of the extracted platform number from the input - """ - # Validate the platform number's format and against the supported versions - platform_number_match = ANDROID_PLATFORM_PATTERN.search(input_android_platform) - if not platform_number_match or not platform_number_match.group(2) or (platform_number_match.group(1) and platform_number_match.group(1) != 'android-'): - raise common.LmbrCmdError(f"Invalid {platform_variable_type} version value ({input_android_platform}). It must be " - f"either 'XX' or android-'XX' where 'XX' is a platform number.", - common.ERROR_CODE_INVALID_PARAMETER) - - android_platform_number = int(platform_number_match.group(2)) - if android_platform_number < min_version: - raise common.LmbrCmdError(f"Invalid {platform_variable_type} version value ({input_android_platform}) is less than the minimum " - f"supported version ({min_version}).", - common.ERROR_CODE_INVALID_PARAMETER) - if android_platform_number > max_version: - raise common.LmbrCmdError(f"Invalid {platform_variable_type} version value ({input_android_platform}) is greater than the maximum " - f"supported version ({max_version}).", - common.ERROR_CODE_INVALID_PARAMETER) - return android_platform_number - - ANDROID_SDK_ENV_NAME = 'ANDROID_SDK' -ANDROID_SDK_MIN_PLATFORM = 28 -ANDROID_SDK_MAX_PLATFORM = 29 - - -def verify_android_sdk(android_sdk_platform, argument_name, override_android_sdk_path=None, preferred_sdk_build_tools_ver=None): - """ - Verify the android sdk and the requested platform platform against the android sdk path - - :param android_sdk_platform: The android sdk platform to use (e.g. '28' or 'android-28') - :param argument_name: The name of the argument for descriptive errors to present - :param override_android_sdk_path: The location of the android SDK path if not set through the environment variable - :param preferred_sdk_build_tools_ver: Option prefered built tool version under the android SDK if available. Will fallback to the first one discovered - :returns tuple of the verified android sdk platform number, path to the Android SDK path and the build tool version - """ - android_sdk_platform_number = validate_android_platform_input(input_android_platform=android_sdk_platform, - platform_variable_type='android sdk', - min_version=ANDROID_SDK_MIN_PLATFORM, - max_version=ANDROID_SDK_MAX_PLATFORM) - - # Get the candidate android sdk path from either the override argument or the system environment variable - if override_android_sdk_path: - check_android_sdk_path = override_android_sdk_path - else: - check_android_sdk_path = os.environ.get(ANDROID_SDK_ENV_NAME) - if not check_android_sdk_path: - raise common.LmbrCmdError(f"Android SDK path not set. Make sure that either the '{ANDROID_SDK_ENV_NAME}' environment is " - f"set or it is passed in through the {argument_name} argument") - - # The android sdk folder structure is expected to have a 'platforms' sub folder based on the android sdk-platform number - check_android_sdk_path = pathlib.Path(check_android_sdk_path) - android_sdk_platforms_path = check_android_sdk_path / 'platforms' - if not android_sdk_platforms_path.is_dir(): - raise common.LmbrCmdError(f"Invalid Android SDK path '{str(check_android_sdk_path)}': Missing 'platforms' directory.") - - # Collect the available platform numbers from the platforms subdirectory - validated_android_platforms = [] - for dir_item in android_sdk_platforms_path.iterdir(): - if not dir_item.is_dir(): - continue - check_file = dir_item / 'package.xml' - if check_file.is_file(): - validated_android_platforms.append(dir_item.name) - - if not validated_android_platforms: - raise common.LmbrCmdError(f"Invalid Android SDK path '{str(check_android_sdk_path)}': Unable to find any android platforms.") - - # Normalize the android_sdk argument to fit the same folder name pattern - android_sdk_platform_name = f'android-{android_sdk_platform_number}' - if android_sdk_platform_name not in validated_android_platforms: - raise common.LmbrCmdError(f"Android SDK platform {android_sdk_platform_name} is not a valid for the android SDK located under '{str(check_android_sdk_path)}'") - - # Enumerate through the build tools under android sdk - android_sdk_build_tools_dir = check_android_sdk_path / 'build-tools' - if not android_sdk_build_tools_dir.is_dir(): - raise common.LmbrCmdError(f"Invalid Android SDK path '{str(check_android_sdk_path)}': Unable to find any built-tools folder.") - supported_build_tools = [str(build_tool.name) for build_tool in android_sdk_build_tools_dir.iterdir() if build_tool.is_dir()] - if not supported_build_tools: - raise common.LmbrCmdError(f"Invalid Android SDK path '{str(check_android_sdk_path)}': Unable to find any built-tools.") - if preferred_sdk_build_tools_ver: - if preferred_sdk_build_tools_ver in supported_build_tools: - validated_build_tool = preferred_sdk_build_tools_ver - else: - validated_build_tool = supported_build_tools[0] - logging.warning("Unable to locate android sdk build tool version {preferred_sdk_build_tools_ver}. Defaulting to version {validated_build_tool}") - - else: - validated_build_tool = supported_build_tools[0] - - return android_sdk_platform_number, check_android_sdk_path, validated_build_tool - - -ANDROID_NDK_ENV_NAME = 'ANDROID_NDK' -ANDROID_NDK_MIN_PLATFORM = 21 -ANDROID_NDK_MAX_PLATFORM = 29 -ANDROID_NDK_SOURCE_PROPERTIES_REVISION_PATTERN = re.compile(r'Pkg.Revision\s*=\s*(\d+.\d+.\d+)') - - -def verify_android_ndk(android_ndk_platform, argument_name, override_android_ndk_path=None): - """ - Verify the android ndk and requested platform against the android ndk path - - :param android_ndk_platform: The android ndk platform to use (e.g. '21' or 'android-21') - :param argument_name: The name of the argument for descriptive errors to present - :param override_android_ndk_path: The location of the android NDK path if not set through the environment variable - :returns tuple of the verified android ndk platform number and the Path to the Android SDK path and the - """ - - android_ndk_platform_number = validate_android_platform_input(input_android_platform=android_ndk_platform, - platform_variable_type='android ndk', - min_version=ANDROID_NDK_MIN_PLATFORM, - max_version=ANDROID_NDK_MAX_PLATFORM) - - # Get the candidate android ndk path from either the override argument or the system environment variable - if override_android_ndk_path: - check_android_ndk_path = str(override_android_ndk_path) - else: - check_android_ndk_path = os.environ.get(ANDROID_NDK_ENV_NAME) - if not check_android_ndk_path: - raise common.LmbrCmdError(f"Android NDK path not set. Make sure that either the {ANDROID_NDK_ENV_NAME} environment " - f"is set or it is passed in through the {argument_name} argument") - check_android_ndk_path = pathlib.Path(check_android_ndk_path) - - # Validate the android ndk path - - # Determine the NDK revision by reading the source.properties file - ndk_source_properties_file = check_android_ndk_path / 'source.properties' - if not ndk_source_properties_file.is_file(): - raise common.LmbrCmdError(f"Invalid Android NDK path '{str(check_android_ndk_path)}'. Missing 'source.properties' file.", - common.ERROR_CODE_INVALID_PARAMETER) - ndk_source_properties_file_content = ndk_source_properties_file.read_text(encoding=common.DEFAULT_TEXT_READ_ENCODING, - errors=common.ENCODING_ERROR_HANDLINGS) - - ndk_revision_match = ANDROID_NDK_SOURCE_PROPERTIES_REVISION_PATTERN.search(ndk_source_properties_file_content) - if not ndk_revision_match: - raise common.LmbrCmdError(f"Invalid Android NDK path '{str(check_android_ndk_path)}'. Unable to extract version from 'source.properties' file.", - common.ERROR_CODE_INVALID_PARAMETER) - ndk_revision_number = LooseVersion(ndk_revision_match.group(1)) - logging.info(f"Detected Android NDK Revision {str(ndk_revision_number)}") - - # Collect the supported android platforms from the required 'platforms' folder under the ndk path - android_ndk_platforms_path = check_android_ndk_path / 'platforms' - if not android_ndk_platforms_path.is_dir(): - raise common.LmbrCmdError(f"Invalid Android NDK path '{str(check_android_ndk_path)}'. Missing 'platforms' folder.", - common.ERROR_CODE_INVALID_PARAMETER) - - validated_android_platforms = [] - for dir_item in android_ndk_platforms_path.iterdir(): - if not dir_item.is_dir(): - continue - api_version_match = ANDROID_PLATFORM_PATTERN.search(dir_item.name) - if not api_version_match or api_version_match.group(1) != 'android-': - continue - - check_lib_path = dir_item / 'arch-arm64/usr/lib' - if check_lib_path.is_dir(): - validated_android_platforms.append(dir_item.name) - - # For NDK revisions 19 and up, there is a mapping file for version numbers that map to other version. - platforms_map_aliases = {} - if ndk_revision_number >= LooseVersion('19.0.0'): - platforms_map_file = check_android_ndk_path / 'meta/platforms.json' - if platforms_map_file.exists(): - with open(platforms_map_file, 'r') as platforms_map_file_handle: - platforms_map_file_json = json.load(platforms_map_file_handle) - platforms_map_aliases = platforms_map_file_json['aliases'] - elif validated_android_platforms: - # Revisions before 19 does not have a mapping file for API versions, they fall back to the previous one - # So we need to make a mapping file that does the same - platforms_map_aliases = {} - validated_android_platforms.sort() - max_supported_api_number = int(ANDROID_PLATFORM_PATTERN.search(validated_android_platforms[-1]).group(2)) - for validated_android_platform in validated_android_platforms: - current_api_version = int(ANDROID_PLATFORM_PATTERN.search(validated_android_platform).group(2)) - next_api_version = current_api_version + 1 - while f'android-{next_api_version}' not in validated_android_platforms and next_api_version <= max_supported_api_number: - platforms_map_aliases[str(next_api_version)] = current_api_version - next_api_version += 1 - - # Go through the aliases and add to the validated platforms if it is mapped to an existing platform - for alias_key, alias_value in platforms_map_aliases.items(): - if not ANDROID_PLATFORM_PATTERN.search(f'android-{alias_key}'): - # Skip any non android-XX (XX = number) aliases - continue - aliased_platform_key = f'android-{alias_value}' - if aliased_platform_key in validated_android_platforms: - validated_android_platforms.append(f'android-{alias_key}') - - if not validated_android_platforms: - raise common.LmbrCmdError(f"Invalid Android NDK path {str(check_android_ndk_path)}") - - # Verify the ndk platform against the ndk path - android_ndk_platform_name = f'android-{android_ndk_platform_number}' - if android_ndk_platform_name not in validated_android_platforms: - raise common.LmbrCmdError(f"Android NDK platform {android_ndk_platform_name} is not a valid for the Android NDK located under '{str(check_android_ndk_path)}'") - - return android_ndk_platform_number, check_android_ndk_path - - -ADB_TARGET = 'adb.exe' if platform.system() == 'Windows' else 'adb' def resolve_adb_tool(android_sdk_path): @@ -1528,9 +1353,16 @@ def resolve_adb_tool(android_sdk_path): if isinstance(android_sdk_path, str): android_sdk_path = pathlib.Path(android_sdk_path) - check_adb_target = android_sdk_path / 'platform-tools' / ADB_TARGET - if not check_adb_target.exists(): - raise common.LmbrCmdError(f"Invalid Android SDK path '{str(android_sdk_path)}': Unable to locate '{ADB_TARGET}'.") + file_found = False + for executable_path_ext in common.PLATFORM_EXECUTABLE_EXTENSIONS: + check_adb_target = android_sdk_path / 'platform-tools' / f'adb{executable_path_ext}' + if check_adb_target.is_file(): + file_found = True + break + + if not file_found: + raise common.LmbrCmdError(f"Invalid Android SDK path '{str(android_sdk_path)}': Unable to locate 'adb'.") + return check_adb_target @@ -1633,3 +1465,196 @@ class AdbTool(common.CommandLineExec): else: adb_params = arguments return super().popen(adb_params, cwd) + + +class AndroidGradlePluginInfo(object): + + def __init__(self, android_gradle_plugin_version): + + if android_gradle_plugin_version not in ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP.keys(): + raise common.LmbrCmdError(f"Android Gradle Plugin version {android_gradle_plugin_version} is not supported. " + f"Only the following version(s) are supported: {','.join(ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP.keys())}") + + details = ANDROID_GRADLE_PLUGIN_COMPATIBILITY_MAP[android_gradle_plugin_version] + self.default_sdk_build_tools_version = LooseVersion(details.get('sdk_build')) + + self.default_ndk_version = LooseVersion(details.get('default_ndk')) + + self.min_gradle_version = LooseVersion(details.get('min_gradle_version')) + + self.min_cmake_version = LooseVersion(details.get('min_cmake_version')) + + max_cmake_version_number = details.get('max_cmake_version') + self.max_cmake_version = None if max_cmake_version_number is None else LooseVersion(max_cmake_version_number) + + +class AndroidSDKResolver(object): + """ + Class that manages the Android SDK tool to validate, install packages (e.g. built tools, sdk platforms, ndk, etc) + """ + + class InstalledPackage(object): + def __init__(self, installed_package_components): + assert len(installed_package_components) == 4, '4 sections expected for installed package components (path, version, description, location)' + self.path = installed_package_components[0] + self.version = LooseVersion(installed_package_components[1]) + self.description = installed_package_components[2] + self.location = installed_package_components[3] + + class AvailablePackage(object): + def __init__(self, available_package_components): + assert len(available_package_components) == 3, '3 sections expected for installed package components (path, version, description)' + self.path = available_package_components[0] + self.version = LooseVersion(available_package_components[1]) + self.description = available_package_components[2] + + class AvailableUpdate(object): + def __init__(self, available_update_components): + assert len(available_update_components) == 3, '3 sections expected for installed package components (path, version, available)' + self.path = available_update_components[0] + self.version = LooseVersion(available_update_components[1]) + self.available = available_update_components[2] + + def __init__(self, android_sdk_path): + + self.android_sdk_path = android_sdk_path or os.environ.get(ANDROID_SDK_ENV_NAME) + if not self.android_sdk_path: + raise common.LmbrCmdError(f"Android SDK path not set or it was not passed into the command to generate the android project") + if not os.path.isdir(self.android_sdk_path): + raise common.LmbrCmdError(f"Android SDK path {self.android_sdk_path} is not valid") + if platform.system() == 'Windows': + self.sdk_manager_path = pathlib.Path(self.android_sdk_path) / 'tools' / 'bin' / 'sdkmanager.bat' + else: + raise common.LmbrCmdError(f"This tool is not supported on the current platform {platform.system()}") + if not self.sdk_manager_path.is_file(): + raise common.LmbrCmdError(f"Android SDK path {self.android_sdk_path} is not valid or complete. Missing {self.sdk_manager_path}") + + self.sdk_manager = common.CommandLineExec(str(self.sdk_manager_path.resolve())) + + self.installed_packages = {} + self.available_packages = {} + self.available_updates = {} + self.refresh_sdk_installation() + + def refresh_sdk_installation(self): + """ + Utilize the sdk_manager command line tool from the Android SDK to collect / refresh the list of + installed, available, and updateable packages that are managed by the android SDK. + """ + self.installed_packages = {} + self.available_packages = {} + self.available_updates = {} + + def _factory_installed_package(package_map, item_components): + package_map[item_components[0]] = AndroidSDKResolver.InstalledPackage(item_components) + + def _factory_available_package(package_map, item_components): + package_map[item_components[0]] = AndroidSDKResolver.AvailablePackage(item_components) + + def _factory_available_update(package_map, item_components): + package_map[item_components[0]] = AndroidSDKResolver.AvailableUpdate(item_components) + + # Use the SDK manager to collect the available and installed packages + result_code, result_stdout, result_stderr = self.sdk_manager.exec(['--list'], capture_stdout=True, suppress_stderr=True) + + current_append_map = None + current_item_factory = None + for package_item in result_stdout.split('\n'): + package_item_stripped = package_item.strip() + if not package_item_stripped: + continue + if '|' not in package_item_stripped: + if package_item_stripped.upper() == 'INSTALLED PACKAGES:': + current_append_map = self.installed_packages + current_item_factory = _factory_installed_package + elif package_item_stripped.upper() == 'AVAILABLE PACKAGES:': + current_append_map = self.available_packages + current_item_factory = _factory_available_package + elif package_item_stripped.upper() == 'AVAILABLE UPDATES:': + current_append_map = self.available_updates + current_item_factory = _factory_available_update + else: + current_append_map = None + current_item_factory = None + continue + item_parts = [split.strip() for split in package_item_stripped.split('|')] + if len(item_parts) < 3: + continue + elif item_parts[1].upper() in ('VERSION', 'INSTALLED', '-------'): + continue + elif current_append_map is None: + continue + if current_append_map is not None and current_item_factory is not None: + current_item_factory(current_append_map, item_parts) + + def is_package_installed(self, search_package_path): + """ + Check if a package path to see if its a package that is installed. The path can use wildcard '*'s + The function will return a list of the results that match the package paths, ordered by the newest version first + """ + def _package_sort(package): + return package.version + package_detail_result_list = [] + for installed_package_path, installed_package_details in self.installed_packages.items(): + if fnmatch.fnmatch(installed_package_path, search_package_path): + package_detail_result_list.append(installed_package_details) + package_detail_result_list.sort(reverse=True, key=_package_sort) + return package_detail_result_list + + def is_package_available(self, search_package_path): + """ + Check if a package path to see if its an available package to install. The path can use wildcard '*'s + The function will return a list of the results that match the package paths, ordered by the newest version first + """ + def _package_sort(package): + return package.version + package_detail_result_list = [] + for available_package_path, available_package_details in self.available_packages.items(): + if fnmatch.fnmatch(available_package_path, search_package_path): + package_detail_result_list.append(available_package_details) + package_detail_result_list.sort(reverse=True, key=_package_sort) + return package_detail_result_list + + def install_package(self, package_install_path, package_description): + """ + Install a package based on the path of an available android sdk package + """ + + # Skip installation if the package is already installed + package_result_list = self.is_package_installed(package_install_path) + if package_result_list: + installed_package_detail = package_result_list[0] + logging.info(f"{installed_package_detail.description} (version {installed_package_detail.version}) Detected") + return installed_package_detail + + # Make sure the package name is available + package_result_list = self.is_package_available(package_install_path) + if not package_result_list: + raise common.LmbrCmdError(f"Invalid Android SDK Package {package_description}: Bad package path {package_install_path}") + + # Reverse sort and pick the first item, which should be the latest (if the install path contains wildcards) + def _available_sort(item): + return item.path + + package_result_list.sort(reverse=True, key=_available_sort) + + available_package_to_install = package_result_list[0] # For multiple hits, resolve to the first item which will be the latest version + + # Perform the package installation + logging.info(f"Installing {available_package_to_install.description} ...") + result_code, result_stdout, result_stderr = self.sdk_manager.exec(['--install', available_package_to_install.path], capture_stdout=True, suppress_stderr=True) + if result_code != 0: + raise common.LmbrCmdError(f"Error installing package {available_package_to_install.path}: \n{result_stderr}") + + # Refresh the tracked SDK Contents + self.refresh_sdk_installation() + + # Get the package details to verify + package_result_list = self.is_package_installed(package_install_path) + if package_result_list: + installed_package_detail = package_result_list[0] + logging.info(f"{installed_package_detail.description} (version {installed_package_detail.version}) Installed") + return installed_package_detail + else: + raise common.LmbrCmdError(f"Error installing package {available_package_to_install.path}: \n{result_stderr}") + diff --git a/cmake/Tools/Platform/Android/generate_android_project.py b/cmake/Tools/Platform/Android/generate_android_project.py index 9a0e2760f5..d25b62dde8 100755 --- a/cmake/Tools/Platform/Android/generate_android_project.py +++ b/cmake/Tools/Platform/Android/generate_android_project.py @@ -27,7 +27,7 @@ from cmake.Tools import common from cmake.Tools.Platform.Android import android_support GRADLE_ARGUMENT_NAME = '--gradle-install-path' -GRADLE_MIN_VERSION = LooseVersion('4.10.1') +GRADLE_MIN_VERSION = LooseVersion('6.5') GRADLE_MAX_VERSION = LooseVersion('7.0.0') GRADLE_VERSION_REGEX = re.compile(r"Gradle\s(\d+.\d+.?\d*)") GRADLE_EXECUTABLE = 'gradle.bat' if platform.system() == 'Windows' else 'gradle' @@ -48,9 +48,9 @@ def verify_gradle(override_gradle_path=None): CMAKE_ARGUMENT_NAME = '--cmake-install-path' -CMAKE_MIN_VERSION = LooseVersion('3.17.0') +CMAKE_MIN_VERSION = LooseVersion('3.19.0') CMAKE_VERSION_REGEX = re.compile(r'cmake version (\d+.\d+.?\d*)') -CMAKE_EXECUTABLE = 'cmake.exe' if platform.system() == 'Windows' else 'cmake' +CMAKE_EXECUTABLE = 'cmake' def verify_cmake(override_cmake_path=None): @@ -69,7 +69,7 @@ def verify_cmake(override_cmake_path=None): NINJA_ARGUMENT_NAME = '--ninja-install-path' NINJA_VERSION_REGEX = re.compile(r'(\d+.\d+.?\d*)') -NINJA_EXECUTABLE = 'ninja.exe' if platform.system() == 'Windows' else 'ninja' +NINJA_EXECUTABLE = 'ninja' def verify_ninja(override_ninja_path=None): @@ -78,7 +78,7 @@ def verify_ninja(override_ninja_path=None): """ return common.verify_tool(override_tool_path=override_ninja_path, tool_name='ninja', - tool_filename='ninja.exe' if platform.system() == 'Windows' else 'ninja', + tool_filename='ninja', argument_name=NINJA_ARGUMENT_NAME, tool_version_argument='--version', tool_version_regex=NINJA_VERSION_REGEX, @@ -103,13 +103,21 @@ def build_optional_signing_profile(store_file, store_password, key_alias, key_pa ANDROID_SDK_ARGUMENT_NAME = '--android-sdk-path' -ANDROID_SDK_PLATFORM_ARGUMENT_NAME = '--android-sdk-version' +ANDROID_SDK_PLATFORM_ARGUMENT_NAME = '--android-sdk-platform' ANDROID_SDK_PREFERRED_TOOL_VER = '--android-sdk-build-tool-version' +ANDROID_NATIVE_API_LEVEL = '--android-native-api-level' + + +MIN_ANDROID_SDK_PLATFORM = 28 # The minimum platform/api level that is supported for the SDK Platform +MIN_NATIVE_API_LEVEL = 24 # The minimum Native API level that is supported for the NDK + -ANDROID_NDK_ARGUMENT_NAME = '--android-ndk-path' ANDROID_NDK_PLATFORM_ARGUMENT_NAME = '--android-ndk-version' +ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME = '--gradle-plugin-version' +ANDROID_GRADLE_MIN_PLUGIN_VERSION = LooseVersion("4.2.0") + # Constants for asset-related options for APK generation INCLUDE_APK_ASSETS_ARGUMENT_NAME = "--include-apk-assets" ASSET_MODE_ARGUMENT_NAME = "--asset-mode" @@ -147,6 +155,7 @@ def main(args): parser = argparse.ArgumentParser(description="Prepare the android studio subfolder") + # Required Arguments parser.add_argument('--engine-root', help='The path to the engine root. Defaults to the current working directory.', default=os.getcwd()) @@ -160,32 +169,42 @@ def main(args): help='The path to the 3rd Party root directory', required=True) - parser.add_argument(ANDROID_NDK_ARGUMENT_NAME, - help='The path to the android NDK', - required=True) - parser.add_argument(ANDROID_SDK_ARGUMENT_NAME, help='The path to the android SDK', required=True) - parser.add_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME, - help='The android SDK version', + parser.add_argument('-g', '--project-path', + help='The project path to generate an android project', required=True) + parser.add_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME, + help=f'The android SDK platform number version to use for the APK. (Minimum {MIN_ANDROID_SDK_PLATFORM})', + type=int, + default=-1) + + parser.add_argument(ANDROID_NATIVE_API_LEVEL, + help=f'The android native API level to use for the APK. If not set, this will default to the android SDK platform. (Minimum {MIN_ANDROID_SDK_PLATFORM})', + type=int, + default=-1) + + # Override arguments parser.add_argument(ANDROID_SDK_PREFERRED_TOOL_VER, - help='The preferred android sdk build version (i.e. 28.0.3). Will default to the first one detected under the android sdk', - default=None, + help='The android SDK build tools version.', required=False) parser.add_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME, help='The android NDK version', - required=True) + required=False) parser.add_argument(GRADLE_ARGUMENT_NAME, help=f'The path to installed gradle. The version of gradle must fall in between {str(GRADLE_MIN_VERSION)} and {str(GRADLE_MAX_VERSION)}.', default=None, required=False) + parser.add_argument(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME, + help=f'The version of the android gradle plugin to use. Defaults to the minimum version ({ANDROID_GRADLE_MIN_PLUGIN_VERSION})', + default=str(ANDROID_GRADLE_MIN_PLUGIN_VERSION)) + parser.add_argument(CMAKE_ARGUMENT_NAME, help=f'The path to cmake build tool if not installed on the system path. The version of cmake must be at least version {str(CMAKE_MIN_VERSION)}.', default=None, @@ -196,9 +215,6 @@ def main(args): default=None, required=False) - parser.add_argument('-g', '--project-path', - help='The project path to generate an android project') - # Asset Options parser.add_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME, action='store_true', @@ -207,11 +223,11 @@ def main(args): parser.add_argument(ASSET_MODE_ARGUMENT_NAME, choices=ALL_ASSET_MODES, default=ASSET_MODE_LOOSE, - help='Asset Mode (vfs|pak|loose) to use when including assets into the APK') + help=f'Asset Mode (vfs|pak|loose) to use when including assets into the APK. (Defaults to {ASSET_MODE_LOOSE})') parser.add_argument(ASSET_TYPE_ARGUMENT_NAME, default=DEFAULT_ASSET_TYPE, - help='Asset Type to use when including assets into the APK') + help=f'Asset Type to use when including assets into the APK. (Defaults to {DEFAULT_ASSET_TYPE})') parser.add_argument('--debug', action='store_true', @@ -260,16 +276,81 @@ def main(args): ninja_version, override_ninja_path = verify_ninja(override_ninja_path=parsed_args.get_argument(NINJA_ARGUMENT_NAME)) logging.info("Detected Ninja version %s", str(ninja_version)) - # Verify the android sdk path and sdk version - verified_android_sdk_platform, verified_android_sdk_path, android_sdk_build_tool_ver = android_support.verify_android_sdk(android_sdk_platform=parsed_args.get_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME), - argument_name=ANDROID_SDK_ARGUMENT_NAME, - override_android_sdk_path=parsed_args.get_argument(ANDROID_SDK_ARGUMENT_NAME), - preferred_sdk_build_tools_ver=parsed_args.get_argument(ANDROID_SDK_PREFERRED_TOOL_VER)) + # Get the android sdk platform version to use from the arguments, but also handle the deprecated argument name + android_sdk_platform_version = parsed_args.get_argument(ANDROID_SDK_PLATFORM_ARGUMENT_NAME) - # Verify the android ndk path and ndk version - verified_android_ndk_platform, verified_android_ndk_path = android_support.verify_android_ndk(android_ndk_platform=parsed_args.get_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME), - argument_name=ANDROID_NDK_ARGUMENT_NAME, - override_android_ndk_path=parsed_args.get_argument(ANDROID_NDK_ARGUMENT_NAME)) + # Get the gradle plugin details and validate against the current environment + android_gradle_plugin_version = parsed_args.get_argument(ANDROID_GRADLE_PLUGIN_ARGUMENT_NAME) + android_gradle_plugin = android_support.AndroidGradlePluginInfo(android_gradle_plugin_version) + logging.info(f"Generating Android Gradle Plugin version {android_gradle_plugin_version} based project") + + if gradle_version < android_gradle_plugin.min_gradle_version: + raise common.LmbrCmdError(f"The current version of gradle ({gradle_version}) does not satisfy the minimum version " + f"({android_gradle_plugin.min_gradle_version}) needed for the android gradle plugin " + f"({android_gradle_plugin_version}). Please upgrade your gradle.") + if cmake_version < android_gradle_plugin.min_cmake_version: + raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) does not satisfy the minimum version " + f"({android_gradle_plugin.min_cmake_version}) needed for the android gradle plugin " + f"({android_gradle_plugin_version}). Please upgrade your cmake.") + if android_gradle_plugin.max_cmake_version and cmake_version > android_gradle_plugin.max_cmake_version: + raise common.LmbrCmdError(f"The current version of cmake ({cmake_version}) exceeds the maximum version " + f"({android_gradle_plugin.max_cmake_version}) of the android gradle plugin " + f"({android_gradle_plugin_version}).") + + # Use the SDK Resolver to make sure the build tools and ndk + android_sdk = android_support.AndroidSDKResolver(android_sdk_path=parsed_args.get_argument(ANDROID_SDK_ARGUMENT_NAME)) + + # If no SDK platform is provided, check for any installed one + if android_sdk_platform_version < 0: + android_sdk_platform_version = MIN_ANDROID_SDK_PLATFORM + installed_android_sdk_platforms = android_sdk.is_package_installed('platforms;*') + if installed_android_sdk_platforms: + # If there are installed platforms, check the most recent one + latest_platform_version = -1 + for installed_android_sdk_platform in installed_android_sdk_platforms: + platform_number_match = re.match(r'platforms;android-([0-9]*)', installed_android_sdk_platform.path) + if not platform_number_match: + continue + check_platform_version = int(platform_number_match.group(1)) + if check_platform_version > latest_platform_version: + latest_platform_version = check_platform_version + if latest_platform_version >= MIN_ANDROID_SDK_PLATFORM: + android_sdk_platform_version = latest_platform_version + else: + if android_sdk_platform_version < MIN_ANDROID_SDK_PLATFORM: + raise common.LmbrCmdError(f"Invalid argument for {ANDROID_SDK_PLATFORM_ARGUMENT_NAME} ({android_sdk_platform_version}). Must be greater than the minimum value supported {MIN_ANDROID_SDK_PLATFORM}.") + + # Get the android native api level from the arguments. Default to the sdk platform version if not provided + android_native_api_level = parsed_args.get_argument(ANDROID_NATIVE_API_LEVEL) + if android_native_api_level < 0: + android_native_api_level = android_sdk_platform_version + else: + if android_native_api_level < MIN_NATIVE_API_LEVEL: + raise common.LmbrCmdError(f"Invalid argument for {ANDROID_NATIVE_API_LEVEL} ({android_native_api_level}). Must be greater than the minimum value supported {MIN_NATIVE_API_LEVEL}.") + + # Check and make sure that the requested sdk platform exists, download if necessary + platform_package_name = f"platforms;android-{android_sdk_platform_version}" + android_sdk.install_package(package_install_path=platform_package_name, + package_description=f'Android SDK Platform {android_sdk_platform_version}') + + # Make sure we have the extra android packages "market_apk_expansion" and "market_licensing" which is needed by the APK + android_sdk.install_package(package_install_path='extras;google;market_apk_expansion', + package_description='Google APK Expansion Library') + + android_sdk.install_package(package_install_path='extras;google;market_licensing', + package_description='Google Play Licensing Library') + + # Install either the requested SDK build tools or the default one for the android gradle plugin version + build_tools_version = parsed_args.get_argument(ANDROID_SDK_PREFERRED_TOOL_VER) or android_gradle_plugin.default_sdk_build_tools_version + build_tools_package_name = f'build-tools;{build_tools_version}' + build_tools_package = android_sdk.install_package(package_install_path=build_tools_package_name, + package_description='Android SDK Build Tools') + + # Install either the requested NDK version or the default one for the android gradle plugin version + android_ndk_version = parsed_args.get_argument(ANDROID_NDK_PLATFORM_ARGUMENT_NAME) or android_gradle_plugin.default_ndk_version + android_ndk_package_name = f'ndk;{android_ndk_version}' + android_ndk_package = android_sdk.install_package(package_install_path=android_ndk_package_name, + package_description='Android NDK') # Verify the engine root path and project path verified_project_path, verified_engine_root = common.verify_project_and_engine_root(project_root=parsed_args.project_path, @@ -277,10 +358,9 @@ def main(args): is_test_project = parsed_args.unit_test # Verify the 3rd Party Root Path - third_party_path = pathlib.Path(parsed_args.third_party_path) / '3rdParty.txt' - if not third_party_path.is_file(): - raise common.LmbrCmdError("Invalid --third-party-path '{}'. Make sure it exists and contains " - "3rdParty.txt".format(parsed_args.third_party_path), + third_party_path = pathlib.Path(parsed_args.third_party_path) + if not third_party_path.is_dir(): + raise common.LmbrCmdError(f"Invalid --third-party-path '{parsed_args.third_party_path}'.", common.ERROR_CODE_INVALID_PARAMETER) third_party_path = third_party_path.parent @@ -293,23 +373,23 @@ def main(args): logging.debug("Engine Root : %s", str(verified_engine_root.resolve())) logging.debug("Build Path : %s", str(build_dir.resolve())) - logging.debug("Android NDK Path : %s", str(verified_android_ndk_path.resolve())) - logging.debug("Android SDK Path : %s", str(verified_android_sdk_path.resolve())) # Prepare the generator and execute generator = android_support.AndroidProjectGenerator(engine_root=verified_engine_root, - project_path=verified_project_path, build_dir=build_dir, - android_sdk_path=verified_android_sdk_path, - android_ndk_path=verified_android_ndk_path, - android_sdk_version=verified_android_sdk_platform, - android_ndk_platform=verified_android_ndk_platform, + android_sdk_path=android_sdk.android_sdk_path, + build_tool=build_tools_package, + android_sdk_platform=android_sdk_platform_version, + android_native_api_level=android_native_api_level, + android_ndk=android_ndk_package, + project_path=verified_project_path, third_party_path=third_party_path, cmake_version=cmake_version, override_cmake_path=override_cmake_path, override_gradle_path=override_gradle_path, + gradle_version=gradle_version, + gradle_plugin_version=android_gradle_plugin_version, override_ninja_path=override_ninja_path, - android_sdk_build_tool_version=android_sdk_build_tool_ver, include_assets_in_apk=parsed_args.get_argument(INCLUDE_APK_ASSETS_ARGUMENT_NAME), asset_mode=parsed_args.get_argument(ASSET_MODE_ARGUMENT_NAME), asset_type=parsed_args.get_argument(ASSET_TYPE_ARGUMENT_NAME), diff --git a/cmake/Tools/Platform/Android/unit_test_generate_android_project.py b/cmake/Tools/Platform/Android/unit_test_generate_android_project.py index 5598942046..0cd0f16eaf 100755 --- a/cmake/Tools/Platform/Android/unit_test_generate_android_project.py +++ b/cmake/Tools/Platform/Android/unit_test_generate_android_project.py @@ -170,117 +170,3 @@ def test_verify_ninja(tmpdir, from_override, version_str, expected_result): finally: subprocess.check_output = orig_check_output - -TEST_VALIDATE_VERSION_MIN = 19 -TEST_VALIDATE_VERSION_MAX = 21 - - -@pytest.mark.parametrize( - "test_input, expected", [ - pytest.param('20', 20), - pytest.param('android-20', 20), - pytest.param('bad-21', "android-'XX'"), - pytest.param('10', "minimum"), - pytest.param('30', "maximum") - ] -) -def test_validate_android_platform_input(test_input, expected): - try: - result = android_support.validate_android_platform_input(input_android_platform=test_input, - platform_variable_type='test', - min_version=TEST_VALIDATE_VERSION_MIN, - max_version=TEST_VALIDATE_VERSION_MAX) - assert isinstance(expected, int) - assert result == expected - except Exception as e: - assert expected in str(e) - - -def test_verify_android_sdk_success(tmpdir): - - test_android_path = 'android_sdk' - sdk_version_number = 28 - sdk_version = f'android-{sdk_version_number}' - - tmpdir.ensure(f'{test_android_path}/platforms/{sdk_version}/package.xml') - - tmpdir.ensure(f'{test_android_path}/build-tools/28.0.3/package.xml') - tmpdir.ensure(f'{test_android_path}/build-tools/29.0.3/package.xml') - - input_sdk_path = tmpdir.join(test_android_path).realpath() - argument_name = '--android-sdk' - - requested_build_tool_version = '29.0.3' - - result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version, - argument_name=argument_name, - override_android_sdk_path=input_sdk_path, - preferred_sdk_build_tools_ver=requested_build_tool_version) - assert result_sdk_version == sdk_version_number - assert result_sdk_path == input_sdk_path - assert result_build_tool_version == requested_build_tool_version - - sdk_version_number_only = str(sdk_version_number) - result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version_number_only, - argument_name=argument_name, - override_android_sdk_path=input_sdk_path) - assert result_sdk_version == sdk_version_number - assert result_sdk_path == input_sdk_path - assert result_build_tool_version == '28.0.3' - - requested_build_tool_version = '30.0.3' - result_sdk_version, result_sdk_path, result_build_tool_version = android_support.verify_android_sdk(android_sdk_platform=sdk_version, - argument_name=argument_name, - override_android_sdk_path=input_sdk_path, - preferred_sdk_build_tools_ver=requested_build_tool_version) - assert result_sdk_version == sdk_version_number - assert result_sdk_path == input_sdk_path - assert result_build_tool_version == '28.0.3' - - -@pytest.mark.parametrize( - "desired_ndk_version_number, available_ndk_revisions, pkg_revision, mappings, expect_error", [ - pytest.param(21, [21, 22, 24], '15.2.4203891', None, False, id='preNdk19ExactMatch'), - pytest.param(23, [21, 22, 24], '15.2.4203891', None, False, id='preNdk19FallbackMatch'), - pytest.param(22, [21, 22, 24], '19.2.4203891', {'23': 21}, False, id='postNdk19ExactMatch'), - pytest.param(23, [21, 22, 24], '21.2.4203891', {'23': 21}, False, id='postNdk19MappingMatch'), - pytest.param(android_support.ANDROID_NDK_MIN_PLATFORM-1, [21, 22, 24], '15.2.4203891', None, True, id='preNdk19BelowMinVer'), - pytest.param(android_support.ANDROID_NDK_MAX_PLATFORM+1, [21, 22, 24], '15.2.4203891', None, True, id='preNdk19AboveMaxVer'), - pytest.param(25, [21, 22, 24], '19.2.4203891', {'23': 21}, True, id='postNdk19NoMatch') - ] -) -def test_verify_android_ndk_success(tmpdir, desired_ndk_version_number, available_ndk_revisions, pkg_revision, mappings, expect_error): - - test_android_path = 'android_ndk' - for ndk_number in available_ndk_revisions: - tmpdir.ensure(f'{test_android_path}/platforms/android-{ndk_number}/arch-arm64/usr/lib/libc.so') - - tmpdir.ensure(f'{test_android_path}/source.properties') - test_ndk_source_properties_file = tmpdir / test_android_path / 'source.properties' - test_ndk_source_properties_file.write_text(f'Pkg.Desc = Android NDK\nPkg.Revision = {pkg_revision}\n', encoding='ASCII') - - if mappings: - platform_mapping = { - # min and max are arbitrary for now since we dont use it during evaluation, but if we do, parameterize it here as well - "min": 16, # - "max": 29, - "aliases": {} - } - for key, value in mappings.items(): - platform_mapping['aliases'][key] = value - tmpdir.ensure(f'{test_android_path}/meta/platforms.json') - platform_mapping_file = tmpdir / test_android_path / 'meta/platforms.json' - platform_mapping_file.write_text(json.dumps(platform_mapping), encoding='ASCII') - - input_ndk_path = tmpdir.join(test_android_path).realpath() - - try: - android_ndk_platform_number, android_ndk_path = android_support.verify_android_ndk(android_ndk_platform=str(desired_ndk_version_number), - argument_name="--android-ndk", - override_android_ndk_path=input_ndk_path) - assert not expect_error - assert android_ndk_platform_number == desired_ndk_version_number - assert android_ndk_path == input_ndk_path - except Exception: - assert expect_error - diff --git a/cmake/Tools/common.py b/cmake/Tools/common.py index c6a3e89e67..9c0d31cd53 100755 --- a/cmake/Tools/common.py +++ b/cmake/Tools/common.py @@ -55,6 +55,7 @@ ENGINE_ROOT_CHECK_FILE = 'engine.json' HASH_CHUNK_SIZE = 200000 + class LmbrCmdError(Exception): """ Wrapper class to the general exception class where will absorb and prevent the printing of stack. @@ -244,6 +245,19 @@ def load_template_file(template_file_path, template_env): raise FileNotFoundError(f"Invalid file path. Cannot find template file located at {str(template_file_path)}") +# Determine the possible file extensions for executable files based on the host platform +PLATFORM_EXECUTABLE_EXTENSIONS = [''] # Files without extensions are always considered + +if platform.system() == 'Windows': + # Windows manages its executable extensions through the %PATHEXT% environment variable + path_extensions_str = os.environ.get('PATHEXT', default='.EXE;.COM;.BAT;.CMD') + PLATFORM_EXECUTABLE_EXTENSIONS.extend([pathext.lower() for pathext in path_extensions_str.split(';')]) +elif platform.system() == 'Linux': + PLATFORM_EXECUTABLE_EXTENSIONS = ['', '.out'] +else: + PLATFORM_EXECUTABLE_EXTENSIONS = [''] + + def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, tool_version_argument, tool_version_regex, min_version, max_version): """ Support method to validate a required system tool needed for the build either through an installed tool in the @@ -270,12 +284,21 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too elif not isinstance(override_tool_path, pathlib.Path): raise LmbrCmdError(f"Invalid {tool_name} path argument. '{override_tool_path}' must be a string or Path", ERROR_CODE_INVALID_PARAMETER) - check_tool_path = override_tool_path / tool_filename - if not check_tool_path.is_file(): - check_tool_path = pathlib.Path(override_tool_path) / 'bin' / tool_filename + file_found = False + for executable_path_ext in PLATFORM_EXECUTABLE_EXTENSIONS: + check_tool_filename = f'{tool_filename}{executable_path_ext}' - if not check_tool_path.is_file(): + check_tool_path = override_tool_path / check_tool_filename + if check_tool_path.is_file(): + file_found = True + break + check_tool_path = override_tool_path / 'bin' / check_tool_filename + if check_tool_path.is_file(): + file_found = True + break + + if not file_found: raise LmbrCmdError(f"Invalid {tool_name} path argument. '{override_tool_path}' is not a valid {tool_name} path", ERROR_CODE_INVALID_PARAMETER) resolved_override_tool_path = str(check_tool_path.resolve()) @@ -284,7 +307,7 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too else: resolved_override_tool_path = None tool_source = tool_name - tool_desc = "installed gradle in the system path" + tool_desc = f"installed {tool_name} in the system path" # Extract the version and verify version_output = subprocess.check_output([tool_source, tool_version_argument], @@ -296,10 +319,10 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too result_version = LooseVersion(str(version_match.group(1)).strip()) if min_version and result_version < min_version: - raise LmbrCmdError(f"The {tool_desc} does not meet the minimum version of gradle required ({str(min_version)}).", + raise LmbrCmdError(f"The {tool_desc} does not meet the minimum version of {tool_name} required ({str(min_version)}).", ERROR_CODE_ENVIRONMENT_ERROR) elif max_version and result_version > max_version: - raise LmbrCmdError(f"The {tool_desc} exceeds maximum version of gradle supported ({str(max_version)}).", + raise LmbrCmdError(f"The {tool_desc} exceeds maximum version of {tool_name} supported ({str(max_version)}).", ERROR_CODE_ENVIRONMENT_ERROR) return result_version, resolved_override_tool_path diff --git a/scripts/build/Platform/Android/build_config.json b/scripts/build/Platform/Android/build_config.json index adaa417380..b871670cd0 100644 --- a/scripts/build/Platform/Android/build_config.json +++ b/scripts/build/Platform/Android/build_config.json @@ -141,10 +141,8 @@ "COMMAND":"gradle_windows.cmd", "PARAMETERS": { "CONFIGURATION":"profile", - "OUTPUT_DIRECTORY":"build\\android_gradle", + "OUTPUT_DIRECTORY":"build\\ad_grd", "GAME_PROJECT": "AutomatedTesting", - "ANDROID_NDK_PLATFORM": "21", - "ANDROID_SDK_PLATFORM": "29", "SIGN_APK": "false", "GRADLE_BUILD_CMD": "build", "ADDITIONAL_GENERATE_ARGS": "" @@ -158,8 +156,6 @@ "CONFIGURATION":"profile", "OUTPUT_DIRECTORY":"build\\android_unittest", "GAME_PROJECT": "AutomatedTesting", - "ANDROID_NDK_PLATFORM": "21", - "ANDROID_SDK_PLATFORM": "29", "SIGN_APK": "true", "GRADLE_BUILD_CMD": "assemble", "ADDITIONAL_GENERATE_ARGS": "--unit-test" diff --git a/scripts/build/Platform/Android/gradle_windows.cmd b/scripts/build/Platform/Android/gradle_windows.cmd index 56423af95a..dd5285bdbf 100644 --- a/scripts/build/Platform/Android/gradle_windows.cmd +++ b/scripts/build/Platform/Android/gradle_windows.cmd @@ -17,20 +17,12 @@ IF NOT EXIST "%LY_3RDPARTY_PATH%" ( GOTO :error ) -IF NOT EXIST "%GRADLE_HOME%" ( +IF NOT EXIST "%GRADLE_BUILD_HOME%" ( REM This is the default for developers - SET GRADLE_HOME=C:\Gradle\gradle-5.6.4 + SET GRADLE_BUILD_HOME=C:\Gradle\gradle-7.0 ) -IF NOT EXIST "%GRADLE_HOME%" ( - ECHO [ci_build] FAIL: GRADLE_HOME=%GRADLE_HOME% - GOTO :error -) - -IF NOT EXIST "%CMAKE_HOME%" ( - SET CMAKE_HOME=%LY_3RDPARTY_PATH%/CMake/3.19.1/Windows/ -) -IF NOT EXIST "%CMAKE_HOME%" ( - ECHO [ci_build] FAIL: CMAKE_HOME=%CMAKE_HOME% +IF NOT EXIST "%GRADLE_BUILD_HOME%" ( + ECHO [ci_build] FAIL: GRADLE_BUILD_HOME=%GRADLE_BUILD_HOME% GOTO :error ) @@ -50,20 +42,9 @@ ECHO Ninja wasnt in the call path, add the value set by LY_NINJA_PATH SET PATH=%PATH%;%LY_NINJA_PATH% :ninja_on_path -IF NOT EXIST "%LY_ANDROID_SDK%" ( - SET LY_ANDROID_SDK=!LY_3RDPARTY_PATH!/android-sdk/platform-29 -) -IF NOT EXIST "%LY_ANDROID_SDK%" ( - ECHO [ci_build] FAIL: LY_ANDROID_SDK=!LY_ANDROID_SDK! - GOTO :error -) -IF NOT EXIST "%LY_ANDROID_NDK%" ( - set LY_ANDROID_NDK=!LY_3RDPARTY_PATH!/android-ndk/r21d -) -IF NOT EXIST "%LY_ANDROID_NDK%" ( - ECHO [ci_build] LY_ANDROID_NDK=!LY_ANDROID_NDK! - GOTO :error +IF NOT "%ANDROID_GRADLE_PLUGIN%" == "" ( + set ANDROID_GRADLE_PLUGIN_OPTION=--gradle-plugin-version=%ANDROID_GRADLE_PLUGIN% ) IF NOT EXIST %OUTPUT_DIRECTORY% ( @@ -154,11 +135,11 @@ IF "%GENERATE_SIGNED_APK%"=="true" ( ECHO Using keystore file at %CI_ANDROID_KEYSTORE_FILE_ABS% ) - ECHO [ci_build] %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_HOME% --cmake-install-path=%CMAKE_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-ndk-path=%LY_ANDROID_NDK% --android-sdk-path=%LY_ANDROID_SDK% --android-ndk-version=%ANDROID_NDK_PLATFORM% --android-sdk-version=%ANDROID_SDK_PLATFORM% --signconfig-store-file %CI_ANDROID_KEYSTORE_FILE_ABS% --signconfig-store-password %CI_ANDROID_KEYSTORE_PASSWORD% --signconfig-key-alias %CI_ANDROID_KEYSTORE_ALIAS% --signconfig-key-password %CI_ANDROID_KEYSTORE_PASSWORD% %OPTIONAL_TEST_FLAG% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing - CALL %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_HOME% --cmake-install-path=%CMAKE_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-ndk-path=%LY_ANDROID_NDK% --android-sdk-path=%LY_ANDROID_SDK% --android-ndk-version=%ANDROID_NDK_PLATFORM% --android-sdk-version=%ANDROID_SDK_PLATFORM% --signconfig-store-file %CI_ANDROID_KEYSTORE_FILE_ABS% --signconfig-store-password %CI_ANDROID_KEYSTORE_PASSWORD% --signconfig-key-alias %CI_ANDROID_KEYSTORE_ALIAS% --signconfig-key-password %CI_ANDROID_KEYSTORE_PASSWORD% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing + ECHO [ci_build] %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_BUILD_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-sdk-path=%ANDROID_HOME% %ANDROID_GRADLE_PLUGIN_OPTION% --signconfig-store-file %CI_ANDROID_KEYSTORE_FILE_ABS% --signconfig-store-password %CI_ANDROID_KEYSTORE_PASSWORD% --signconfig-key-alias %CI_ANDROID_KEYSTORE_ALIAS% --signconfig-key-password %CI_ANDROID_KEYSTORE_PASSWORD% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing + CALL %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_BUILD_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-sdk-path=%ANDROID_HOME% %ANDROID_GRADLE_PLUGIN_OPTION% --signconfig-store-file %CI_ANDROID_KEYSTORE_FILE_ABS% --signconfig-store-password %CI_ANDROID_KEYSTORE_PASSWORD% --signconfig-key-alias %CI_ANDROID_KEYSTORE_ALIAS% --signconfig-key-password %CI_ANDROID_KEYSTORE_PASSWORD% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing ) ELSE ( - ECHO [ci_build] %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_HOME% --cmake-install-path=%CMAKE_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-ndk-path=%LY_ANDROID_NDK% --android-sdk-path=%LY_ANDROID_SDK% --android-ndk-version=%ANDROID_NDK_PLATFORM% --android-sdk-version=%ANDROID_SDK_PLATFORM% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing - CALL %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_HOME% --cmake-install-path=%CMAKE_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% --android-ndk-path=%LY_ANDROID_NDK% --android-sdk-path=%LY_ANDROID_SDK% --android-ndk-version=%ANDROID_NDK_PLATFORM% --android-sdk-version=%ANDROID_SDK_PLATFORM% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing + ECHO [ci_build] %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% %GRADLE_OVERRIDE_OPTION% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% %ANDROID_GRADLE_PLUGIN_OPTION% --android-sdk-path=%ANDROID_HOME% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing + CALL %PYTHON% cmake\Tools\Platform\Android\generate_android_project.py --engine-root=. --build-dir=%OUTPUT_DIRECTORY% -g %GAME_PROJECT% --gradle-install-path=%GRADLE_BUILD_HOME% --ninja-install-path=%LY_NINJA_PATH% --third-party-path=%LY_3RDPARTY_PATH% %ANDROID_GRADLE_PLUGIN_OPTION% --android-sdk-path=%ANDROID_HOME% %ADDITIONAL_GENERATE_ARGS% --overwrite-existing ) REM Validate the android project generation diff --git a/scripts/build/Platform/Android/pipeline.json b/scripts/build/Platform/Android/pipeline.json index ed10e7022d..551374a027 100644 --- a/scripts/build/Platform/Android/pipeline.json +++ b/scripts/build/Platform/Android/pipeline.json @@ -1,7 +1,7 @@ { "ENV": { - "GRADLE_HOME": "C:/Gradle/gradle-5.6.4", - "NODE_LABEL": "windows-047e5cdf", + "GRADLE_HOME": "C:/Gradle/gradle-7.0", + "NODE_LABEL": "windows-b3c8994f1", "LY_3RDPARTY_PATH": "C:/ly/3rdParty", "TIMEOUT": 30, "WORKSPACE": "D:/workspace", From 370f28f69cc06a471534c69f691e3b8e906669c1 Mon Sep 17 00:00:00 2001 From: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> Date: Tue, 1 Jun 2021 01:28:13 -0700 Subject: [PATCH 30/53] Missing PHYSX_ENABLE_MULTI_THREADING for PhysX.Editor --- Gems/PhysX/Code/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gems/PhysX/Code/CMakeLists.txt b/Gems/PhysX/Code/CMakeLists.txt index b0318af9f2..d281270ce4 100644 --- a/Gems/PhysX/Code/CMakeLists.txt +++ b/Gems/PhysX/Code/CMakeLists.txt @@ -128,6 +128,9 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) AUTOMOC FILES_CMAKE physx_editor_shared_files.cmake + COMPILE_DEFINITIONS + PUBLIC + PHYSX_ENABLE_MULTI_THREADING INCLUDE_DIRECTORIES PRIVATE . From 3947dcf213e0b5326d7d33b25868e2363497869a Mon Sep 17 00:00:00 2001 From: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> Date: Tue, 1 Jun 2021 09:36:00 +0100 Subject: [PATCH 31/53] Add some extra cvars to control orbit point appearance and remove unused ones (#1032) --- .../AzFramework/Viewport/CameraInput.cpp | 3 +-- .../ModularViewportCameraController.cpp | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp index d5f02c957c..559f7ce460 100644 --- a/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp +++ b/Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp @@ -29,7 +29,7 @@ namespace AzFramework AZ_CVAR(float, ed_cameraSystemOrbitDollyScrollSpeed, 0.02f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemOrbitDollyCursorSpeed, 0.01f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemScrollTranslateSpeed, 0.02f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); - AZ_CVAR(float, ed_cameraSystemMinOrbitDistance, 6.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); + AZ_CVAR(float, ed_cameraSystemMinOrbitDistance, 10.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemMaxOrbitDistance, 50.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemLookSmoothness, 5.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(float, ed_cameraSystemTranslateSmoothness, 5.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); @@ -37,7 +37,6 @@ namespace AzFramework AZ_CVAR(float, ed_cameraSystemPanSpeed, 0.01f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(bool, ed_cameraSystemPanInvertX, true, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR(bool, ed_cameraSystemPanInvertY, true, nullptr, AZ::ConsoleFunctorFlags::Null, ""); - AZ_CVAR(float, ed_cameraSystemLookDeadzone, 2.0f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); AZ_CVAR( AZ::CVarFixedString, ed_cameraSystemTranslateForwardKey, "keyboard_key_alphanumeric_W", nullptr, AZ::ConsoleFunctorFlags::Null, ""); diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp index 6fb3edfa22..896d9f8043 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/ModularViewportCameraController.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,11 @@ namespace AtomToolsFramework { + AZ_CVAR( + AZ::Color, ed_cameraSystemOrbitPointColor, AZ::Color::CreateFromRgba(255, 255, 255, 255), nullptr, AZ::ConsoleFunctorFlags::Null, + ""); + AZ_CVAR(float, ed_cameraSystemOrbitPointSize, 0.5f, nullptr, AZ::ConsoleFunctorFlags::Null, ""); + // debug void DrawPreviewAxis(AzFramework::DebugDisplayRequests& display, const AZ::Transform& transform, const float axisLength) { @@ -73,7 +79,8 @@ namespace AtomToolsFramework if (auto viewportContext = RetrieveViewportContext(GetViewportId())) { - auto handleCameraChange = [this, viewportContext](const AZ::Matrix4x4&) { + auto handleCameraChange = [this, viewportContext](const AZ::Matrix4x4&) + { if (!m_updatingTransform) { UpdateCameraFromTransform(m_targetCamera, viewportContext->GetCameraTransform()); @@ -137,7 +144,10 @@ namespace AtomToolsFramework } else if (m_cameraMode == CameraMode::Animation) { - const auto smootherStepFn = [](const float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }; + const auto smootherStepFn = [](const float t) + { + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); + }; const float transitionT = smootherStepFn(m_animationT); const AZ::Transform current = AZ::Transform::CreateFromQuaternionAndTranslation( @@ -169,8 +179,9 @@ namespace AtomToolsFramework { if (const float alpha = AZStd::min(-m_camera.m_lookDist / 5.0f, 1.0f); alpha > AZ::Constants::FloatEpsilon) { - debugDisplay.SetColor(1.0f, 1.0f, 1.0f, alpha); - debugDisplay.DrawWireSphere(m_camera.m_lookAt, 0.5f); + const AZ::Color orbitPointColor = ed_cameraSystemOrbitPointColor; + debugDisplay.SetColor(orbitPointColor.GetR(), orbitPointColor.GetG(), orbitPointColor.GetB(), alpha); + debugDisplay.DrawWireSphere(m_camera.m_lookAt, ed_cameraSystemOrbitPointSize); } } From 99ba89a02b82a408089ea55983bab77b78d6ec31 Mon Sep 17 00:00:00 2001 From: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> Date: Tue, 1 Jun 2021 09:37:02 +0100 Subject: [PATCH 32/53] Add console function to print entity name from entity id (#1021) * Add console function to print entity name from entity id * update name of console function an improve description --- .../AzCore/Component/ComponentApplication.cpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp index 1010ae3473..7b33359023 100644 --- a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp +++ b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp @@ -77,6 +77,27 @@ #endif // defined(AZ_ENABLE_DEBUG_TOOLS) #include +#include + +static void PrintEntityName(const AZ::ConsoleCommandContainer& arguments) +{ + if (arguments.empty()) + { + return; + } + + const auto entityIdStr = AZStd::string(arguments.front()); + const auto entityIdValue = AZStd::stoull(entityIdStr); + + AZStd::string entityName; + AZ::ComponentApplicationBus::BroadcastResult( + entityName, &AZ::ComponentApplicationBus::Events::GetEntityName, AZ::EntityId(entityIdValue)); + + AZ_Printf("Entity Debug", "EntityId: %" PRIu64 ", Entity Name: %s", entityIdValue, entityName.c_str()); +} + +AZ_CONSOLEFREEFUNC( + PrintEntityName, AZ::ConsoleFunctorFlags::Null, "Parameter: EntityId value, Prints the name of the entity to the console"); namespace AZ { From c03669df72886ce3c51ecb9d0526976a16fbcc81 Mon Sep 17 00:00:00 2001 From: Aaron Ruiz Mora Date: Tue, 1 Jun 2021 15:16:50 +0100 Subject: [PATCH 33/53] Updating default physics material library with the latest materials (#1056) --- .../surfacetypemateriallibrary.physmaterial | 153 ++++++++++++++++-- 1 file changed, 144 insertions(+), 9 deletions(-) diff --git a/AutomatedTesting/surfacetypemateriallibrary.physmaterial b/AutomatedTesting/surfacetypemateriallibrary.physmaterial index 434d673998..481cd2fbfa 100644 --- a/AutomatedTesting/surfacetypemateriallibrary.physmaterial +++ b/AutomatedTesting/surfacetypemateriallibrary.physmaterial @@ -4,17 +4,152 @@ - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7d1fedc10c442269811ce4531bf9434e653acb2c Mon Sep 17 00:00:00 2001 From: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> Date: Tue, 1 Jun 2021 08:29:35 -0700 Subject: [PATCH 34/53] LYN-4128 EditorPythonBindings.Editor in debug does not load (missing python_d.dll) --- Gems/PythonAssetBuilder/Code/CMakeLists.txt | 26 ++++++------------- .../Common/RuntimeDependencies_common.cmake | 2 +- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/Gems/PythonAssetBuilder/Code/CMakeLists.txt b/Gems/PythonAssetBuilder/Code/CMakeLists.txt index 4af266f56d..73fa90ab67 100644 --- a/Gems/PythonAssetBuilder/Code/CMakeLists.txt +++ b/Gems/PythonAssetBuilder/Code/CMakeLists.txt @@ -13,24 +13,11 @@ if(NOT PAL_TRAIT_BUILD_HOST_TOOLS) return() endif() -set(static_files pythonassetbuilder_common_files.cmake) -set(editor_files pythonassetbuilder_editor_files.cmake) -set(shared_files pythonassetbuilder_shared_files.cmake) -set(static_dependencies - 3rdParty::Python - Gem::EditorPythonBindings.Static - AZ::AssetBuilderSDK -) -set(editor_dependencies - Gem::EditorPythonBindings.Static - AZ::AssetBuilderSDK -) - ly_add_target( NAME PythonAssetBuilder.Static STATIC NAMESPACE Gem FILES_CMAKE - ${static_files} + pythonassetbuilder_common_files.cmake PLATFORM_INCLUDE_FILES Source/Platform/Common/${PAL_TRAIT_COMPILER_ID}/pythonassetbuilder_static_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake INCLUDE_DIRECTORIES @@ -43,7 +30,9 @@ ly_add_target( PRIVATE AZ::AzCore PUBLIC - ${static_dependencies} + 3rdParty::Python + Gem::EditorPythonBindings.Static + AZ::AssetBuilderSDK AZ::AzToolsFramework ) @@ -52,8 +41,8 @@ ly_add_target( NAMESPACE Gem FILES_CMAKE - ${editor_files} - ${shared_files} + pythonassetbuilder_editor_files.cmake + pythonassetbuilder_shared_files.cmake PLATFORM_INCLUDE_FILES Source/Platform/Common/${PAL_TRAIT_COMPILER_ID}/pythonassetbuilder_static_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake INCLUDE_DIRECTORIES @@ -64,7 +53,8 @@ ly_add_target( Include BUILD_DEPENDENCIES PRIVATE - ${editor_dependencies} + Gem::EditorPythonBindings.Static + AZ::AssetBuilderSDK RUNTIME_DEPENDENCIES Gem::EditorPythonBindings.Editor ) diff --git a/cmake/Platform/Common/RuntimeDependencies_common.cmake b/cmake/Platform/Common/RuntimeDependencies_common.cmake index d9d0fe4c7f..4ae914744f 100644 --- a/cmake/Platform/Common/RuntimeDependencies_common.cmake +++ b/cmake/Platform/Common/RuntimeDependencies_common.cmake @@ -105,7 +105,7 @@ function(ly_get_runtime_dependencies ly_RUNTIME_DEPENDENCIES ly_TARGET) set(skip_imported TRUE) endif() endif() - if(target_type MATCHES "(INTERFACE_LIBRARY|STATIC_LIBRARY)") + if(target_type MATCHES "(STATIC_LIBRARY)") # No need to copy these dependencies since the outputs are not used at runtime set(skip_imported TRUE) endif() From 895bbafa9e61cb5366e8a36a74b5b0664b73fcf3 Mon Sep 17 00:00:00 2001 From: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:50:10 -0500 Subject: [PATCH 35/53] Fixed CreatePrefab to use correct absolute path (#1044) The initial CreatePrefab flow was trying to go from absolute -> relative -> absolute path before the file had ever been saved, so the relative -> absolute path conversion generated an error and always produced a project-relative path, even if the initial path was in a gem. For example, trying to save "c:/o3de/Gems/Camera/Assets/Entity1.prefab" would instead create "c:/o3de/AutomatedTesting/Entity1.prefab". This change preserves the absolute path throughout the initial creation flow so that the file is saved in the correct location. --- .../AzToolsFramework/Prefab/PrefabLoader.cpp | 39 +++++++++++++++++++ .../AzToolsFramework/Prefab/PrefabLoader.h | 10 +++++ .../Prefab/PrefabLoaderInterface.h | 10 +++++ .../Prefab/PrefabPublicHandler.cpp | 9 +++-- .../Prefab/PrefabPublicHandler.h | 2 +- .../Prefab/PrefabPublicInterface.h | 4 +- .../UI/Prefab/PrefabIntegrationManager.cpp | 3 +- 7 files changed, 69 insertions(+), 8 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp index e4507227b5..d7de634c11 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.cpp @@ -303,6 +303,45 @@ namespace AzToolsFramework return true; } + bool PrefabLoader::SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath) + { + AZ_Assert(absolutePath.IsAbsolute(), "SaveTemplateToFile requires an absolute path for saving the initial prefab file."); + + const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId); + if (!domAndFilepath) + { + return false; + } + + // Verify that the absolute path provided to this matches the relative path saved in the template. + // Otherwise, the saved prefab won't be able to be loaded. + auto relativePath = GenerateRelativePath(absolutePath); + if (relativePath != domAndFilepath->second) + { + AZ_Error( + "Prefab", false, + "PrefabLoader::SaveTemplateToFile - " + "Failed to save template '%s' to location '%.*s'." + "Error: Relative path '%.*s' for location didn't match template name.", + domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), AZ_STRING_ARG(relativePath.Native())); + return false; + } + + auto outcome = AzFramework::FileFunc::WriteJsonFile(domAndFilepath->first, absolutePath); + if (!outcome.IsSuccess()) + { + AZ_Error( + "Prefab", false, + "PrefabLoader::SaveTemplateToFile - " + "Failed to save template '%s' to location '%.*s'." + "Error: %s", + domAndFilepath->second.c_str(), AZ_STRING_ARG(absolutePath.Native()), outcome.GetError().c_str()); + return false; + } + m_prefabSystemComponentInterface->SetTemplateDirtyFlag(templateId, false); + return true; + } + bool PrefabLoader::SaveTemplateToString(TemplateId templateId, AZStd::string& output) { const auto& domAndFilepath = StoreTemplateIntoFileFormat(templateId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h index aed24e153e..3722e14a97 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoader.h @@ -72,6 +72,16 @@ namespace AzToolsFramework */ bool SaveTemplate(TemplateId templateId) override; + /** + * Saves a Prefab Template to the provided absolute source path, which needs to match the relative path in the template. + * Converts Prefab Template form into .prefab form by collapsing nested Template info + * into a source path and patches. + * @param templateId Id of the template to be saved + * @param absolutePath Absolute path to save the file to + * @return bool on whether the operation succeeded or not + */ + bool SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath) override; + /** * Saves a Prefab Template into the provided output string. * Converts Prefab Template form into .prefab form by collapsing nested Template info diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h index d71fbff80f..0e551cee6b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabLoaderInterface.h @@ -60,6 +60,16 @@ namespace AzToolsFramework */ virtual bool SaveTemplate(TemplateId templateId) = 0; + /** + * Saves a Prefab Template to the provided absolute source path, which needs to match the relative path in the template. + * Converts Prefab Template form into .prefab form by collapsing nested Template info + * into a source path and patches. + * @param templateId Id of the template to be saved + * @param absolutePath Absolute path to save the file to + * @return bool on whether the operation succeeded or not + */ + virtual bool SaveTemplateToFile(TemplateId templateId, AZ::IO::PathView absolutePath) = 0; + /** * Saves a Prefab Template into the provided output string. * Converts Prefab Template form into .prefab form by collapsing nested Template info diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 9dd5199ea2..fadcc1b81f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -64,7 +64,7 @@ namespace AzToolsFramework m_prefabUndoCache.Destroy(); } - PrefabOperationResult PrefabPublicHandler::CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView filePath) + PrefabOperationResult PrefabPublicHandler::CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView absolutePath) { EntityList inputEntityList, topLevelEntities; AZ::EntityId commonRootEntityId; @@ -76,6 +76,8 @@ namespace AzToolsFramework return findCommonRootOutcome; } + AZ_Assert(absolutePath.IsAbsolute(), "CreatePrefab requires an absolute path for saving the initial prefab file."); + InstanceOptionalReference instanceToCreate; { // Initialize Undo Batch object @@ -144,7 +146,8 @@ namespace AzToolsFramework // Create the Prefab instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab( - entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance); + entities, AZStd::move(instancePtrs), m_prefabLoaderInterface->GenerateRelativePath(absolutePath), + commonRootEntityOwningInstance); if (!instanceToCreate) { @@ -254,7 +257,7 @@ namespace AzToolsFramework } // Save Template to file - m_prefabLoaderInterface->SaveTemplate(instanceToCreate->get().GetTemplateId()); + m_prefabLoaderInterface->SaveTemplateToFile(instanceToCreate->get().GetTemplateId(), absolutePath); return AZ::Success(); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index 7e2357dd44..e68a3e0b1e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -46,7 +46,7 @@ namespace AzToolsFramework void UnregisterPrefabPublicHandlerInterface(); // PrefabPublicInterface... - PrefabOperationResult CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView filePath) override; + PrefabOperationResult CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView absolutePath) override; PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult SavePrefab(AZ::IO::Path filePath) override; PrefabEntityResult CreateEntity(AZ::EntityId parentId, const AZ::Vector3& position) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 0750c4d264..2e9152fd1b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -46,10 +46,10 @@ namespace AzToolsFramework * Create a prefab out of the entities provided, at the path provided. * Automatically detects descendants of entities, and discerns between entities and child instances. * @param entityIds The entities that should form the new prefab (along with their descendants). - * @param filePath The path for the new prefab file. + * @param filePath The absolute path for the new prefab file. * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView filePath) = 0; + virtual PrefabOperationResult CreatePrefab(const AZStd::vector& entityIds, AZ::IO::PathView absolutePath) = 0; /** * Instantiate a prefab from a prefab file. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 61d4433c0e..021b97a7dd 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -333,8 +333,7 @@ namespace AzToolsFramework } } - auto createPrefabOutcome = s_prefabPublicInterface->CreatePrefab( - selectedEntities, s_prefabLoaderInterface->GenerateRelativePath(prefabFilePath.data())); + auto createPrefabOutcome = s_prefabPublicInterface->CreatePrefab(selectedEntities, prefabFilePath.data()); if (!createPrefabOutcome.IsSuccess()) { From 71013c383581016f71f9e67ace36c90838d1775a Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Tue, 1 Jun 2021 17:51:06 +0000 Subject: [PATCH 36/53] Rename tests that reference issue numbers to have descriptive names (#913) This also enables one such test that was named after an issue tracker id, that was disabled because of an already resolved issue. --- .../AnimGraphParameterConditionTests.cpp | 25 ++++++------------- .../Code/Tests/BlendTreeBlendNNodeTests.cpp | 4 +-- ...eletionAndRestoreBlendTreeConnections.cpp} | 6 ++--- ...-93621.cpp => CanAdjustGroupParameter.cpp} | 2 +- ...teAnimGraphNode_AnimGraphModelUpdates.cpp} | 6 ++--- ...nRenameParameter_ParameterNodeUpdates.cpp} | 2 +- .../Code/emotionfx_editor_tests_files.cmake | 8 +++--- 7 files changed, 22 insertions(+), 31 deletions(-) rename Gems/EMotionFX/Code/Tests/Bugs/{LY-92860.cpp => CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp} (98%) rename Gems/EMotionFX/Code/Tests/UI/{LY-93621.cpp => CanAdjustGroupParameter.cpp} (95%) rename Gems/EMotionFX/Code/Tests/UI/{LY-92748.cpp => CanDeleteAnimGraphNode_AnimGraphModelUpdates.cpp} (95%) rename Gems/EMotionFX/Code/Tests/UI/{LY-92269.cpp => CanRenameParameter_ParameterNodeUpdates.cpp} (98%) diff --git a/Gems/EMotionFX/Code/Tests/AnimGraphParameterConditionTests.cpp b/Gems/EMotionFX/Code/Tests/AnimGraphParameterConditionTests.cpp index ef3e9820a5..b1ddede059 100644 --- a/Gems/EMotionFX/Code/Tests/AnimGraphParameterConditionTests.cpp +++ b/Gems/EMotionFX/Code/Tests/AnimGraphParameterConditionTests.cpp @@ -10,6 +10,7 @@ * */ +#include "EMotionFX/CommandSystem/Source/AnimGraphParameterCommands.h" #include #include #include @@ -87,26 +88,16 @@ namespace EMotionFX const AnimGraphParameterCondition* condition = GetAnimGraph()->GetParameterCondition(); EXPECT_EQ(condition->GetParameterType(), azrtti_typeid()); - { - AZStd::string result; - EXPECT_TRUE(manager.ExecuteCommand("AnimGraphRemoveParameter -animGraphID 0 -name P0", result)) << result.c_str(); - } - + EXPECT_TRUE(CommandSystem::BuildRemoveParametersCommandGroup(GetAnimGraph(), {"P0"})); EXPECT_EQ(condition->GetParameterType(), azrtti_typeid()); - { - AZStd::string result; - EXPECT_TRUE(manager.ExecuteCommand("AnimGraphRemoveParameter -animGraphID 0 -name P1", result)) << result.c_str(); - } - + EXPECT_TRUE(CommandSystem::BuildRemoveParametersCommandGroup(GetAnimGraph(), {"P1"})); EXPECT_EQ(condition->GetParameterType(), AZ::TypeId::CreateNull()); - // Will be fixed by LY-109269 - //{ - // AZStd::string result; - // EXPECT_TRUE(manager.Undo(result)) << result.c_str(); - //} - - //EXPECT_EQ(condition->GetParameterType(), azrtti_typeid()); + { + AZStd::string result; + EXPECT_TRUE(manager.Undo(result)) << result.c_str(); + } + EXPECT_EQ(condition->GetParameterType(), azrtti_typeid()); } } // namespace EMotionFX diff --git a/Gems/EMotionFX/Code/Tests/BlendTreeBlendNNodeTests.cpp b/Gems/EMotionFX/Code/Tests/BlendTreeBlendNNodeTests.cpp index 657599bcc9..1e910fb62d 100644 --- a/Gems/EMotionFX/Code/Tests/BlendTreeBlendNNodeTests.cpp +++ b/Gems/EMotionFX/Code/Tests/BlendTreeBlendNNodeTests.cpp @@ -264,8 +264,8 @@ namespace EMotionFX BlendTreeBlendNNode* m_blendNNode = nullptr; }; - // Make sure we don't crash when we have no inputs, such as reported by bug LY-114828 - // Also make sure removing connections on BlendN doesn't crash, as reported by LY-114846 + // Make sure we don't crash when we have no inputs + // Also make sure removing connections on BlendN doesn't crash TEST_F(BlendTreeBlendNNodeTests, NoInputsNoCrashTest) { // Remove all input connections of the blendN node. diff --git a/Gems/EMotionFX/Code/Tests/Bugs/LY-92860.cpp b/Gems/EMotionFX/Code/Tests/Bugs/CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp similarity index 98% rename from Gems/EMotionFX/Code/Tests/Bugs/LY-92860.cpp rename to Gems/EMotionFX/Code/Tests/Bugs/CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp index cd4773b221..0d47b86606 100644 --- a/Gems/EMotionFX/Code/Tests/Bugs/LY-92860.cpp +++ b/Gems/EMotionFX/Code/Tests/Bugs/CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp @@ -106,7 +106,7 @@ namespace EMotionFX R"str(AnimGraphCreateConnection -animGraphID 0 -sourceNode Parameters0 -targetNode Smoothing2 -sourcePort 2 -targetPort 0 -startOffsetX 119 -startOffsetY 70 -endOffsetX -2 -endOffsetY 40)str" }; - class LY92860Fixture + class UndoParameterDeletionTests : public CommandRunnerFixture { public: @@ -183,10 +183,10 @@ namespace EMotionFX } }; - TEST_P(LY92860Fixture, ExecuteCommands) + TEST_P(UndoParameterDeletionTests, CanUndoParameterDeletionAndRestoreBlendTreeConnections) { Run(); }; - INSTANTIATE_TEST_CASE_P(LY92860, LY92860Fixture, ::testing::Values(prepareLY92860Commands)); + INSTANTIATE_TEST_CASE_P(UndoParameterDeletionTests, UndoParameterDeletionTests, ::testing::Values(prepareLY92860Commands)); } // EMotionFX diff --git a/Gems/EMotionFX/Code/Tests/UI/LY-93621.cpp b/Gems/EMotionFX/Code/Tests/UI/CanAdjustGroupParameter.cpp similarity index 95% rename from Gems/EMotionFX/Code/Tests/UI/LY-93621.cpp rename to Gems/EMotionFX/Code/Tests/UI/CanAdjustGroupParameter.cpp index 8e4f9a1fc9..4568dd1b03 100644 --- a/Gems/EMotionFX/Code/Tests/UI/LY-93621.cpp +++ b/Gems/EMotionFX/Code/Tests/UI/CanAdjustGroupParameter.cpp @@ -14,7 +14,7 @@ namespace EMotionFX { - INSTANTIATE_TEST_CASE_P(LY93621, CommandRunnerFixture, + INSTANTIATE_TEST_CASE_P(CanAdjustGroupParameter, CommandRunnerFixture, ::testing::Values(std::vector { R"str(CreateAnimGraph)str", R"str(AnimGraphAddGroupParameter -animGraphID 0 -name Group0)str", diff --git a/Gems/EMotionFX/Code/Tests/UI/LY-92748.cpp b/Gems/EMotionFX/Code/Tests/UI/CanDeleteAnimGraphNode_AnimGraphModelUpdates.cpp similarity index 95% rename from Gems/EMotionFX/Code/Tests/UI/LY-92748.cpp rename to Gems/EMotionFX/Code/Tests/UI/CanDeleteAnimGraphNode_AnimGraphModelUpdates.cpp index 9534edb8eb..ad6172d02b 100644 --- a/Gems/EMotionFX/Code/Tests/UI/LY-92748.cpp +++ b/Gems/EMotionFX/Code/Tests/UI/CanDeleteAnimGraphNode_AnimGraphModelUpdates.cpp @@ -21,12 +21,12 @@ namespace EMotionFX { - class LY92748Fixture + class CanDeleteAnimGraphNode : public CommandRunnerFixture { }; - TEST_P(LY92748Fixture, ExecuteCommands) + TEST_P(CanDeleteAnimGraphNode, CanDeleteAnimGraphNode_AnimGraphModelUpdates) { ExecuteCommands(GetParam()); @@ -68,7 +68,7 @@ namespace EMotionFX } - INSTANTIATE_TEST_CASE_P(DISABLED_LY92748, LY92748Fixture, + INSTANTIATE_TEST_CASE_P(CanDeleteAnimGraphNode_AnimGraphModelUpdates, CanDeleteAnimGraphNode, ::testing::Values(std::vector { R"str(CreateAnimGraph)str", R"str(Select -animGraphID 0)str", diff --git a/Gems/EMotionFX/Code/Tests/UI/LY-92269.cpp b/Gems/EMotionFX/Code/Tests/UI/CanRenameParameter_ParameterNodeUpdates.cpp similarity index 98% rename from Gems/EMotionFX/Code/Tests/UI/LY-92269.cpp rename to Gems/EMotionFX/Code/Tests/UI/CanRenameParameter_ParameterNodeUpdates.cpp index 95f42f7e33..fac6f51943 100644 --- a/Gems/EMotionFX/Code/Tests/UI/LY-92269.cpp +++ b/Gems/EMotionFX/Code/Tests/UI/CanRenameParameter_ParameterNodeUpdates.cpp @@ -14,7 +14,7 @@ namespace EMotionFX { - INSTANTIATE_TEST_CASE_P(LY92269, CommandRunnerFixture, + INSTANTIATE_TEST_CASE_P(CanRenameParameter_ParameterNodeUpdates, CommandRunnerFixture, ::testing::Values(std::vector { R"str(CreateAnimGraph)str", R"str(AnimGraphCreateNode -animGraphID 0 -type {A8B5BB1E-5BA9-4B0A-88E9-21BB7A199ED2} -parentName Root -xPos 240 -yPos 230 -name GENERATE -namePrefix BlendTree)str", diff --git a/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake b/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake index 4a7d1414f9..e2670c20f7 100644 --- a/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake +++ b/Gems/EMotionFX/Code/emotionfx_editor_tests_files.cmake @@ -54,15 +54,15 @@ set(FILES Tests/UI/AnimGraphUIFixture.h Tests/UI/MenuUIFixture.cpp Tests/UI/MenuUIFixture.h - Tests/UI/LY-92269.cpp - Tests/UI/LY-92748.cpp - Tests/UI/LY-93621.cpp + Tests/UI/CanRenameParameter_ParameterNodeUpdates.cpp + Tests/UI/CanDeleteAnimGraphNode_AnimGraphModelUpdates.cpp + Tests/UI/CanAdjustGroupParameter.cpp Tests/UI/CanAddJointAndChildren.cpp Tests/Integration/CanAddActor.cpp Tests/Integration/CanAddSimpleMotionComponent.cpp Tests/Integration/CanDeleteJackEntity.cpp Tests/Bugs/CanDeleteMotionWhenMotionIsBeingBlended.cpp - Tests/Bugs/LY-92860.cpp + Tests/Bugs/CanUndoParameterDeletionAndRestoreBlendTreeConnections.cpp Tests/D6JointLimitConfiguration.cpp Tests/D6JointLimitConfiguration.h Tests/Editor/FileManagerTests.cpp From ed55158b35fa6b263a5c0158fe4f384f46c95ff1 Mon Sep 17 00:00:00 2001 From: Fuzzy Carter Date: Tue, 1 Jun 2021 11:03:07 -0700 Subject: [PATCH 37/53] Helios spec-6686 decouple tests (#978) * * Remove test repository references * * Update asset_builder_tests.py docstring to include test steps * * Update test docstrings in asset_bundler_batch_tests.py to include test steps * * Update asset_processor_batch_dependency_tests.py docstrings to contain test steps * * Update asset_processor_batch_dependency_tests2.py docstrings to have test steps * * Update asset_processor_batch_tests.py docstring to include test steps * * Update asset_processor_batch_tests_2.py docstrings to include test steps * Removed a references to a JIRA ticket ID * * Update asset_processor_guit_tests.py docstrings to include test steps * * Update asset_processor_gui_tests_2.py docstrings to have Test Steps * * Update asset_relocator_tests.py docstrings to include test steps * * update missing_dependency_tests.py docstrings to have test steps * * Update auxiliary_content_tests.py docstrings to have test steps * * Update fbx_tests.py docstrings to have test steps. * * Update bank_info_parser_tests.py docstrings to have test steps * * Removed Jira issue ids from Asset Pipline owned code * * Undid two errornous code changes. * * Addressed dbbronso PR-978 feedback. * Steps declared and not populated. 1 - Removed errornous. 2 - Added missing step * Fixed line formatting by removing bad blank line in Docstring * Addressed PR-978 feedback from AMZN-stankowi * Removed commented out entry from cmakefile * Fixed several casing and spelling/typo issues caught in review * Added a missing test step in asset_bundler_batch_tests.py * Removed a test dealing with external projects, cut LYN-4116 to replace * Calarfied test steps in asset_processor_gui_tests.py * Noted test in asset_processor_gui_tests that cannot be ran manually * Clarified test steps for fbx_tests * Added in a comment and whitespace to a disguised operation. Co-authored-by: stankowi --- .../asset_processor_tests/CMakeLists.txt | 11 - .../asset_builder_tests.py | 9 + .../asset_bundler_batch_tests.py | 108 ++++++++- .../asset_processor_batch_dependency_tests.py | 17 ++ ...asset_processor_batch_dependency_tests2.py | 9 + .../asset_processor_batch_tests.py | 155 +++++++++++++ .../asset_processor_batch_tests_2.py | 97 +++----- .../asset_processor_gui_tests.py | 79 ++++++- .../asset_processor_gui_tests_2.py | 48 +++- .../asset_relocator_tests.py | 92 +++++++- .../missing_dependency_tests.py | 142 ++++++++++-- .../auxiliary_content_tests.py | 10 + .../assetpipeline/fbx_tests/fbx_tests.py | 30 +++ .../bank_info_parser_tests.py | 209 ++++++++++++++---- 14 files changed, 882 insertions(+), 134 deletions(-) diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/CMakeLists.txt index a2002f2d15..2e7516db27 100644 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/CMakeLists.txt @@ -128,16 +128,5 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) RUNTIME_DEPENDENCIES AZ::AssetProcessorBatch ) - -# Need performance improvements LYN-1218 -# ly_add_pytest( -# NAME AssetPipelineTests.AssetRelocator -# PATH ${CMAKE_CURRENT_LIST_DIR}/asset_relocator_tests.py -# EXCLUDE_TEST_RUN_TARGET_FROM_IDE -# TEST_SUITE periodic -# TEST_SERIAL -# RUNTIME_DEPENDENCIES -# AZ::AssetProcessorBatch -# ) endif() diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_builder_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_builder_tests.py index e3d52e8260..fb82f180c6 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_builder_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_builder_tests.py @@ -64,6 +64,15 @@ class TestsAssetBuilder_WindowsAndMac(object): ): """ Verifying -debug parameter for AssetBuilder + + Test Steps: + 1. Create temporary workspace + 2. Launch Asset Processor GUI + 3. Add test assets to workspace + 4. Run Asset Builder with debug on an intact slice + 5. Check Asset Builder didn't fail to build + 6. Run Asset Builder with debug on a corrupted slice + 7. Verify corrupted slice produced an error """ env = ap_setup_fixture intact_slice_failed = False diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_bundler_batch_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_bundler_batch_tests.py index 1043bbaefa..e1091b9b82 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_bundler_batch_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_bundler_batch_tests.py @@ -80,6 +80,8 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): def test_WindowsAndMac_RunHelpCmd_ZeroExitCode(self, workspace, bundler_batch_helper): """ Simple calls to all AssetBundlerBatch --help to make sure a non-zero exit codes are returned. + + Test will call each Asset Bundler Batch sub-command with help and will error on a non-0 exit code """ bundler_batch_helper.call_bundlerbatch(help="") bundler_batch_helper.call_seeds(help="") @@ -98,6 +100,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): r""" Tests that an asset list created maps dependencies correctly. testdependencieslevel\level.pak and lists of known dependencies are used for validation + + Test Steps: + 1. Create an asset list from the level.pak + 2. Create Lists of expected assets in the level.pak + 3. Add lists of expected assets to a single list + 4. Compare list of expected assets to actual assets """ helper = bundler_batch_helper @@ -300,6 +308,15 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Validates destructive overwriting for asset lists and that generating debug information does not affect asset list creation + + 1. Create an asset list from seed_list + 2. Validate asset list was created + 3. Read and store contents of asset list into memory + 4. Attempt to create a new asset list in without using --allowOverwrites + 5. Verify that Asset Bundler returns false + 6. Verify that file contents of the orignally created asset list did not change from what was stored in memory + 7. Attempt to create a new asset list without debug while allowing overwrites + 8. Verify that file contents of the orignally created asset list changed from what was stored in memory """ helper = bundler_batch_helper seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list @@ -375,6 +392,14 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Validates bundle creation both through the 'bundles' and 'bundlesettings' subcommands. + + Test Steps: + 1. Create an asset list + 2. Create a bundle with the asset list and without a bundle settings file + 3. Create a bundle with the asset list and a bundle settings file + 4. Validate calling bundle doesn't perform destructive overwrite without --allowOverwrites + 5. Calling bundle again with --alowOverwrites performs destructive overwrite + 6. Validate contents of original bundle and overwritten bundle """ helper = bundler_batch_helper seed_list = os.path.join(workspace.paths.engine_root(), "Assets", "Engine", "SeedAssetList.seed") # Engine seed list @@ -457,6 +482,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Creates bundles using the same asset list and compares that they are created equally. Also validates that platform bundles exclude/include an expected file. (excluded for WIN, included for MAC) + + Test Steps: + 1. Create an asset list + 2. Create bundles for both PC & Mac + 3. Validate that bundles were created + 4. Verify that expected missing file is not in windows bundle + 5. Verify that expected file is in the mac bundle + 6. Create duplicate bundles with allowOverwrites + 7. Verify that files were generated + 8. Verify original bundle checksums are equal to new bundle checksums """ helper = bundler_batch_helper # fmt:off @@ -571,6 +606,24 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Validates that the 'seeds' subcommand can add and remove seeds and seed platforms properly. Also checks that destructive overwrites require the --allowOverwrites flag + + Test Steps: + + 1. Create a PC Seed List from a test asset + 2. Validate that seed list was generated with proper platform flag + 3. Add Mac & PC as platforms to the seed list + 4. Verify that seed has both Mac & PC platform flags + 5. Remove Mac as a platform from the seed list + 6. Verify that seed only has PC as a platform flag + 7. Attempt to add a platform without using the --platform argument + 8. Verify that asset bundler returns False and file contents did not change + 9. Add Mac platform via --addPlatformToSeeds + 10. Validate that seed has both Mac & PC platform flags + 11. Attempt to remove platform without specifying a platform + 12. Validate that seed has both Mac & PC platform flags + 13. Validate that seed list contents did not change + 14. Remove seed + 15. Validate that seed was removed from the seed list """ helper = bundler_batch_helper @@ -692,6 +745,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Tests asset list comparison, both by file and by comparison type. Uses a set of controlled test assets to compare resulting output asset lists + + 1. Create comparison rules files + 2. Create seed files for different sets of test assets + 3. Create assetlist files for seed files + 4. Validate assetlists were created properly + 5. Compare using comparison rules files and just command line arguments """ helper = bundler_batch_helper env = ap_setup_fixture @@ -1021,6 +1080,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): """ Tests that assetlists are created equivalent to the output while being created, and makes sure overwriting an existing file without the --allowOverwrites fails + + Test Steps: + 1. Check that Asset List creation requires PC platform flag + 2. Create a PC Asset List using asset info file and default seed lists using --print + 3. Validate all assets output are present in the asset list + 4. Create a seed file + 5. Attempt to overwrite Asset List without using --allowOverwrites + 6. Validate that command returned an error and file contents did not change + 7. Specifying platform but not "add" or "remove" should fail + 8. Verify file Has changed """ helper = bundler_batch_helper @@ -1102,7 +1171,16 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): def test_WindowsAndMac_AP_BundleProcessing_BundleProcessedAtRuntime(self, workspace, bundler_batch_helper, asset_processor, request): # fmt:on - """Test to make sure the AP GUI will process a newly created bundle file""" + """ + Test to make sure the AP GUI will process a newly created bundle file + + Test Steps: + 1. Make asset list file (used for bundle creation) + 2. Start Asset Processor GUI + 3. Make bundle in /Bundles + 4. Validate file was created in Bundles folder + 5. Make sure bundle now exists in cache + """ # Set up helpers and variables helper = bundler_batch_helper @@ -1131,6 +1209,8 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): addSeed=level_pak, assetListFile=helper["asset_info_file_request"], ) + + # Run Asset Processor GUI result, _ = asset_processor.gui_process() assert result, "AP GUI failed" @@ -1155,6 +1235,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): @pytest.mark.assetpipeline # fmt:off def test_WindowsAndMac_FilesMarkedSkip_FilesAreSkipped(self, workspace, bundler_batch_helper): + """ + Test Steps: + 1. Create an asset list with a file marked as skip + 2. Verify file was created + 3. Verify that only the expected assets are present in the created asset list + """ expected_assets = [ "ui/canvases/lyshineexamples/animation/multiplesequences.uicanvas", "ui/textures/prefab/button_normal.sprite" @@ -1178,6 +1264,12 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): # fmt:off def test_WindowsAndMac_AssetListSkipOneOfTwoParents_SharedDependencyIsIncluded(self, workspace, bundler_batch_helper): + """ + Test Steps: + 1. Create Asset List with a parent asset that is skipped + 2. Verify that Asset List was created + 3. Verify that only the expected assets are present in the asset list + """ expected_assets = [ "testassets/bundlerskiptest_grandparent.dynamicslice", "testassets/bundlerskiptest_parenta.dynamicslice", @@ -1206,6 +1298,13 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): @pytest.mark.assetpipeline # fmt:off def test_WindowsAndMac_AssetLists_SkipRoot_ExcludesAll(self, workspace, bundler_batch_helper): + """ + Negative scenario test that skips the same file being used as the parent seed. + + Test Steps: + 1. Create an asset list that skips the root asset + 2. Verify that asset list was not generated + """ result, _ = bundler_batch_helper.call_assetLists( assetListFile=bundler_batch_helper['asset_info_file_request'], @@ -1222,6 +1321,13 @@ class TestsAssetBundlerBatch_WindowsAndMac(object): @pytest.mark.assetpipeline # fmt:off def test_WindowsAndMac_AssetLists_SkipUniversalWildcard_ExcludesAll(self, workspace, bundler_batch_helper): + """ + Negative scenario test that uses the all wildcard when generating an asset list. + + Test Steps: + 1. Create an Asset List while using the universal all wildcard "*" + 2. Verify that asset list was not generated + """ result, _ = bundler_batch_helper.call_assetLists( assetListFile=bundler_batch_helper['asset_info_file_request'], diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py index e329846554..0c6924f3a2 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py @@ -67,7 +67,19 @@ class TestsAssetProcessorBatch_DependenycyTests(object): libs/materialeffects/surfacetypes.xml is listed as an entry engine_dependencies.xml libs/materialeffects/surfacetypes.xml is not listed as a missing dependency in the 'assetprocessorbatch' console output + + Test Steps: + 1. Assets are pre-processed + 2. Verify that engine_dependencies.xml exists + 3. Verify engine_dependencies.xml has surfacetypes.xml present + 4. Run Missing Dependency scanner against the engine_dependenciese.xml + 5. Verify that Surfacetypes.xml is NOT in the missing depdencies output + 6. Add the schema file which allows our xml parser to understand dependencies for our engine_dependencies file + 7. Process assets + 8. Run Missing Dependency scanner against the engine_dependenciese.xml + 9. Verify that surfacetypes.xml is in the missing dependencies out """ + env = ap_setup_fixture BATCH_LOG_PATH = env["ap_batch_log_file"] asset_processor.create_temp_asset_root() @@ -137,6 +149,11 @@ class TestsAssetProcessorBatch_DependenycyTests(object): def test_WindowsMacPlatforms_BatchCheckSchema_ValidateErrorChecking(self, workspace, asset_processor, ap_setup_fixture, folder, schema): # fmt:on + """ + Test Steps: + 1. Run the Missing Dependency Scanner against everything + 2. Verify that there are no missing dependencies. + """ env = ap_setup_fixture def missing_dependency_log_lines(log) -> [str]: diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests2.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests2.py index 4f33e0df4e..f184ff2392 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests2.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests2.py @@ -60,6 +60,15 @@ class TestsAssetProcessorBatch_DependenycyTests(object): Verify that Schemas can be loaded via Gems utilizing the fonts schema :returns: None + + Test Steps: + 1. Run Missing Dependency Scanner against %fonts%.xml when no fonts are present + 2. Verify fonts are scanned + 3. Verify that missing dependencies are found for fonts + 4. Add fonts to game project + 5. Run Missing Dependency Scanner against %fonts%.xml when fonts are present + 6. Verify that same amount of fonts are scanned + 7. Verify that there are no missing dependencies. """ schema_name = "Font.xmlschema" asset_processor.create_temp_asset_root() diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests.py index 0d830b39e2..3efd9e7fce 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests.py @@ -100,6 +100,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.BAT @pytest.mark.assetpipeline def test_RunAPBatch_TwoPlatforms_ExitCodeZero(self, asset_processor): + """ + Tests Process assets for PC & Mac and verifies that processing exited without error + + Test Steps: + 1. Add Mac and PC as enabled platforms + 2. Process Assets + 3. Validate that AP exited cleanly + """ asset_processor.create_temp_asset_root() asset_processor.enable_asset_processor_platform("pc") asset_processor.enable_asset_processor_platform("mac") @@ -111,6 +119,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id('C1571826') def test_RunAPBatch_OnlyIncludeInvalidAssets_NoAssetsAdded(self, asset_processor, ap_setup_fixture): + """ + Tests processing invalid assets and validating that no assets were moved to the cache + + Test Steps: + 1. Create a test environment with invalid assets + 2. Run asset processor + 3. Validate that no assets were found in the cache + """ asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], "test_ProcessAssets_OnlyIncludeInvalidAssets_NoAssetsAdded") result, _ = asset_processor.batch_process() @@ -127,6 +143,16 @@ class TestsAssetProcessorBatch_AllPlatforms(object): "recognized as failing in the logs. There appears to be a window where the AutoFailJob doesn't complete" "before the shutdown completes and the failure doesn't end up counting") def test_ProcessAssets_IncludeTwoAssetsWithSameProduct_FailingOnSecondAsset(self, asset_processor, ap_setup_fixture): + """ + Tests processing two source assets with the same product file and validates that the second source will error + + Test Steps: + 1. Create a test environment that has two source files with the same product + 2. Run asset processor + 3. Validate that 1 asset failed to process + 4. Validate that only one product file with the expected name is found in the cache + """ + asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], "test_ProcessAssets_IncludeTwoAssetsWithSameProduct_FailingOnSecondAsset") result, output = asset_processor.batch_process(capture_output = True, expect_failure = True) @@ -143,6 +169,17 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id('C1587615') def test_ProcessAndDeleteCache_APBatchShouldReprocess(self, asset_processor, ap_setup_fixture): + """ + Tests processing once, deleting the generated cache, then processing again and validates the cache is created + + Test Steps: + 1. Run asset processor + 2. Compare the cache with expected output + 3. Delete Cache + 4. Compare the cache with expected output to verify that cache is gone + 5. Run asset processor with fastscan disabled + 6. Compare the cache with expected output + """ # Deleting assets from Cache will make them re-processed in AP (after start) # Copying test assets to project folder and deleting them from cache to make sure APBatch will process them @@ -174,6 +211,18 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id('C1591564') def test_ProcessAndChangeSource_APBatchShouldReprocess(self, asset_processor, ap_setup_fixture): + """ + Tests reprocessing of a modified asset and verifies that it was reprocessed + + Test Steps: + 1. Prepare test environment and copy test asset over + 2. Run asset processor + 3. Verify asset processed + 4. Verify asset is in cache + 4. Modify asset + 5. Re-run asset processor + 6. Verify asset was processed + """ # AP Batch Processing changed files (after start) # Copying test assets to project folder and deleting them from cache to make sure APBatch will process them @@ -208,6 +257,18 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.BAT @pytest.mark.assetpipeline def test_ProcessByBothApAndBatch_Md5ShouldMatch(self, asset_processor, ap_setup_fixture): + """ + Tests that a cache generated by AP GUI is the same as AP Batch + + Test Steps: + 1. Create test environment with test assets + 2. Call asset processor batch + 3. Get checksum for file cache + 4. Clean up test environment + 5. Call asset processor gui with quitonidle + 6. Get checksum for file cache + 7. Verify that checksums are equal + """ # AP Batch and AP app processed assets MD5 sums should be the same # Copying test assets to project folder and deleting them from cache to make sure APBatch will process them @@ -240,6 +301,16 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id('C1612446') def test_AddSameAssetsDifferentNames_ShouldProcess(self, asset_processor, ap_setup_fixture): + """ + Tests Asset Processing of duplicate assets with different names and verifies that both assets are processed + + Test Steps: + 1. Create test environment with two identical source assets with different names + 2. Run asset processor + 3. Verify that assets didn't fail to process + 4. Verify the correct number of jobs were performed + 5. Verify that product files are in the cache + """ # Feed two similar slices and texture with different names - should process without any issues # Copying test assets to project folder and deleting them from cache to make sure APBatch will process them @@ -277,6 +348,19 @@ class TestsAssetProcessorBatch_AllPlatforms(object): "recognized as failing in the logs. There appears to be a window where the AutoFailJob doesn't complete" "before the shutdown completes and the failure doesn't end up counting") def test_AddTwoTexturesWithSameName_ShouldProcessAfterRename(self, asset_processor, ap_setup_fixture): + """ + Tests processing of two textures with the same name then verifies that AP will successfully process after + renaming one of the textures + + Test Steps: + 1. Create test environment with two textures that have the same name + 2. Launch Asset Processor + 3. Validate that Asset Processor generates an error + 4. Rename texture files + 5. Run asset processor + 6. Verify that asset processor does not error + 7. Verify that expected product files are in the cache + """ # Feed two different textures with same name (but different extensions) - ap will fail # Rename one of textures and failure should go away @@ -312,6 +396,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.BAT @pytest.mark.assetpipeline def test_InvalidServerAddress_Warning_Logs(self, asset_processor): + """ + Tests running Asset Processor with an invalid server address and verifies that AP returns a warning about + an invalid server address + + Test Steps: + 1. Launch asset processor while providing an invalid server address + 2. Verify asset processor does not fail + 3. Verify that asset processor generated a warning informing the user about an invalid server address + """ asset_processor.create_temp_asset_root() # Launching AP and making sure that the warning exists @@ -327,6 +420,12 @@ class TestsAssetProcessorBatch_AllPlatforms(object): def test_AllSupportedPlatforms_IncludeValidAssets_AssetsProcessed(self, asset_processor, ap_setup_fixture): """ AssetProcessorBatch is successfully processing newly added assets + + Test Steps: + 1. Create a test environment with test assets + 2. Launch Asset Processor + 3. Verify that asset processor does not fail to process + 4. Verify assets are not missing from the cache """ env = ap_setup_fixture @@ -350,6 +449,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object): def test_AllSupportedPlatforms_DeletedAssets_DeletedFromCache(self, asset_processor, ap_setup_fixture): """ AssetProcessor successfully deletes cached items when removed from project + + Test Steps: + 1. Create a test environment with test assets + 2. Run asset processor + 3. Verify expected assets are in the cache + 4. Delete test assets + 5. Run asset processor + 6. Verify expected assets are in the cache """ env = ap_setup_fixture @@ -385,6 +492,10 @@ class TestsAssetProcessorBatch_AllPlatforms(object): """ Tests that when cache is deleted (no cache) and AssetProcessorBatch runs, it successfully starts and processes assets. + + Test Steps: + 1. Run asset processor + 2. Verify asset processor exits cleanly """ asset_processor.create_temp_asset_root() @@ -402,6 +513,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object): # fmt:on """ AssetProcessor successfully recovers assets from cache when deleted. + + Test Steps: + 1. Create test enviornment with test assets + 2. Run Asset Processor and verify it exits cleanly + 3. Make sure cache folder was generated + 4. Delete temp cache assets but leave database behind + 5. Run asset processor and verify it exits cleanly + 6. Verify expected files were generated in the cache """ env = ap_setup_fixture @@ -434,6 +553,14 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.assetpipeline # fmt:off def test_AllSupportedPlatforms_RunFastScanOnEmptyCache_FullScanRuns(self, ap_setup_fixture, asset_processor): + """ + Tests fast scan processing on an empty cache and verifies that a full analyis will be peformed + + Test Steps: + 1. Create a test environment + 2. Execute asset processor batch with fast scan enabled + 3. Verify that a full analysis is performed + """ # fmt:on env = ap_setup_fixture asset_processor.create_temp_asset_root() @@ -455,6 +582,11 @@ class TestsAssetProcessorBatch_AllPlatforms(object): """ After running the APBatch and AP GUI, Logs directory should exist (C1564055), JobLogs, Batch log, and GUI log should exist in the logs directory (C1564056) + + Test Steps: + 1. Run asset processor batch + 2. Run asset processor gui with quit on idle + 3. Verify that logs exist for both AP Batch & AP GUI """ asset_processor.create_temp_asset_root() LOG_PATH = { @@ -536,6 +668,11 @@ class TestsAssetProcessorBatch_AllPlatforms(object): """ Utilizing corrupted test assets, run the batch process to verify the AP logs the failure to process the corrupted file. + + Test Steps: + 1. Create test environment with corrupted slice + 2. Launch Asset Processor + 3. Verify that asset processor fails to process corrupted slice """ env = ap_setup_fixture error_line_found = False @@ -552,6 +689,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.BAT @pytest.mark.assetpipeline def test_validateDirectPreloadDependency_Found(self, asset_processor, ap_setup_fixture, workspace): + """ + Tests processing an asset with a circular dependency and verifies that Asset Processor will return an error + notifying the user about a circular dependency. + + Test Steps: + 1. Create test environment with an asset that has a circular dependency + 2. Launch asset processor + 3. Verify that error is returned informing the user that the asset has a circular dependency + """ env = ap_setup_fixture error_line_found = False @@ -567,6 +713,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object): @pytest.mark.BAT @pytest.mark.assetpipeline def test_validateNestedPreloadDependency_Found(self, asset_processor, ap_setup_fixture, workspace): + """ + Tests processing of a nested circular dependency and verifies that Asset Processor will return an error + notifying the user about a circular depdency + + Test Steps: + 1. Create test environment with an asset that has a nested circular dependency + 2. Launch asset processor + 3. Verify that error is returned informing the user that the asset has a circular dependency + """ env = ap_setup_fixture error_line_found = False diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests_2.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests_2.py index 5c42af2139..fec5df8eb7 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests_2.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_tests_2.py @@ -80,6 +80,15 @@ class TestsAssetProcessorBatch_AllPlatforms(object): # fmt:on """ Tests that fast scan mode can be used and is faster than full scan mode. + + Test Steps: + 1. Ensure all assets are processed + 2. Run Asset Processor without fast scan and measure the time it takes to run + 3. Capture Full Analysis was performed and number of assets processed + 4. Run Asset Processor with full scan and measure the time it takes to run + 5. Capture Full Analysis wans't performed and number of assets processed + 6. Verify that fast scan was faster than full scan + 7. Verify that full scan scanned more assets """ asset_processor.create_temp_asset_root() @@ -111,76 +120,23 @@ class TestsAssetProcessorBatch_AllPlatforms(object): assert full_scan_time > fast_scan_time, "Fast scan was slower that full scan" assert full_scan_analysis[0] > fast_scan_analysis[0], "Full scan did not process more assets than fast scan" - @pytest.mark.test_case_id("C18787404") - @pytest.mark.BAT - @pytest.mark.assetpipeline - @pytest.mark.skip(reason="External project is currently broken.") # LY-119863 - def test_AllSupportedPlatforms_ExternalProject_APRuns(self, workspace, ap_external_project_setup_fixture): - - external_resources = ap_external_project_setup_fixture - logger.info(f"Running external project test at path {external_resources['project_dir']}") - # Delete existing "external project" build if it exists - if os.path.exists(external_resources["project_dir"]): - fs.delete([external_resources["project_dir"]], True, True) - - # fmt:off - assert not os.path.exists(external_resources["project_dir"]), \ - f'{external_resources["project_dir"]} was not deleted' - # fmt:on - - lmbr_cmd = [ - workspace.paths.lmbr(), - "projects", - "create", - external_resources["project_name"], - "--template", - "EmptyTemplate", - "--app-root", - external_resources["project_dir"], - ] - - logger.info(f"Running lmbr projects create command '{lmbr_cmd}'") - - try: - subprocess.check_call(lmbr_cmd) - except subprocess.CalledProcessError as e: - assert False, f"lmbr projects create failed\n{e.stderr}" - - logger.info("...lmbr finished") - assert os.path.exists(external_resources["project_dir"]), "Project folder was not created" - - # AssetProcessor for new External project. Uses mock workspace to emulate external project workspace - external_ap = AssetProcessor(external_resources["external_workspace"]) - - # fmt:off - assert external_ap.batch_process(fastscan=False), \ - "Asset Processor Batch failed on external project" - # fmt:on - - # Parse log looking for errors or failures - log = APLogParser(workspace.paths.ap_batch_log()) - failures, errors = log.runs[-1]["Failures"], log.runs[-1]["Errors"] - assert failures == 0, f"There were {failures} asset processing failures" - assert errors == 0, f"There were {errors} asset processing errors" - - # Check that project cache was created (DNE until AP makes it) - project_cache = os.path.join(external_resources["project_dir"], "Cache") - assert os.path.exists(project_cache), f"{project_cache} was not created by AP" - - # Clean up external project - fs.delete([external_resources["project_dir"]], True, True) - - # fmt:off - assert not os.path.exists(external_resources["project_dir"]), \ - f"{external_resources['project_dir']} was not deleted" - # fmt:on - @pytest.mark.test_case_id("C4874121") @pytest.mark.BAT @pytest.mark.assetpipeline @pytest.mark.parametrize("clear_type", ["rewrite", "delete_asset", "delete_dir"]) def test_AllSupportedPlatforms_DeleteBadAssets_BatchFailedJobsCleared( self, workspace, request, ap_setup_fixture, asset_processor, clear_type): + """ + Tests the ability of Asset Processor to recover from processing of bad assets by removing them from scan folder + + Test Steps: + 1. Create testing environment with good and multiple bad assets + 2. Run Asset Processor + 3. Verify that bad assets fail to process + 4. Fix a bad asset & delete the others + 5. Run Asset Processor + 6. Verify Asset Processor does not have any asset failues + """ env = ap_setup_fixture error_search_terms = ["WWWWWWWWWWWW"] @@ -250,6 +206,14 @@ class TestsAssetProcessorBatch_Windows(object): Verify the AP batch and Gui can run and process assets independent of the Editor We do not want or need to kill running Editors here as they can be involved in other tests or simply being run locally in this branch or another + + Test Steps: + 1. Create temporary testing environment + 2. Run asset processor GUI + 3. Verify AP GUI doesn't error + 4. Stop AP GUI + 5. Run Asset Processor Batch with Fast Scan + 5. Verify Asset Processor Batch exits cleanly """ asset_processor.create_temp_asset_root() @@ -272,6 +236,11 @@ class TestsAssetProcessorBatch_Windows(object): """ Request a run for an invalid platform "AssetProcessor: Error: Platform in config file or command line 'notaplatform'" should be present in the logs + + Test Steps: + 1. Create temporary testing environment + 2. Run Asset Processor with an invalid platform + 3. Check that asset processor returns an Error notifying the user that the invalid platform is not supported """ asset_processor.create_temp_asset_root() error_search_terms = 'AssetProcessor: Error: The list of enabled platforms in the settings registry does not contain platform ' \ diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests.py index ed5651755c..88fa1a77b4 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests.py @@ -77,6 +77,13 @@ class TestsAssetProcessorGUI_Windows(object): def test_SendInputOnControlChannel_ReceivedAndResponded(self, asset_processor): """ Test that the control channel connects and that communication works both directions + + Test Steps: + 1. Start Asset Processor + 2. Send a Ping message to Asset Processor + 3. Listen for Asset Processor response + 4. Verify Asset Processor responds + 5. Stop asset Processor """ asset_processor.create_temp_asset_root() @@ -129,7 +136,15 @@ class TestsAssetProcessorGUI_Windows(object): # fmt:on """ Asset Processor Deletes processed assets when source is removed from project folder (while running) + + Test Steps: + 1. Create a temporary test environment + 2. Run Asset Processor GUI set to stay open on idle and verify that it does not fail + 3. Verify that assets were copied to the cache + 4. Delete the source test asset directory + 5. Verify assets are deleted from the cache """ + env = ap_setup_fixture # Copy test assets to project folder and verify test assets folder exists @@ -170,7 +185,18 @@ class TestsAssetProcessorGUI_Windows(object): # fmt:on """ Processing changed files (while running) + + Test Steps: + 1. Create temporary test environment with test assets + 2. Open Asset Processor GUI with set to stay open after idle and verify it does not fail + 3. Verify contents of source asset for later comparison + 4. Verify contents of product asset for later comparison + 5. Modify contents of source asset + 6. Wait for Asset Processor to go back to idle state + 7. Verify contents of source asset are the modified version + 8. Verify contents of product asset are the modified version """ + env = ap_setup_fixture # Copy test assets to project folder and verify test assets folder exists @@ -184,7 +210,7 @@ class TestsAssetProcessorGUI_Windows(object): result, _ = asset_processor.gui_process(quitonidle=False) assert result, "AP GUI failed" - # Verify contents of test asset in project folder before modication + # Verify contents of test asset in project folder before modification with open(project_asset_path, "r") as project_asset_file: assert project_asset_file.read() == "before_state" @@ -217,7 +243,14 @@ class TestsAssetProcessorGUI_Windows(object): def test_WindowsPlatforms_RunAP_ProcessesIdle(self, asset_processor): """ Asset Processor goes idle + + Test Steps: + 1. Create a temporary testing evnironment + 2. Run Asset Processor GUI without quitonidle + 3. Verify AP Goes Idle + 4. Verify AP goes below 1% CPU usage """ + CPU_USAGE_THRESHOLD = 1.0 # CPU usage percentage delimiting idle from active CPU_USAGE_WIND_DOWN = 10 # Time allowed in seconds for idle processes to stop using CPU @@ -245,7 +278,16 @@ class TestsAssetProcessorGUI_Windows(object): ): """ Processing newly added files to project folder (while running) + + Test Steps: + 1. Create a temporary testing environment with test assets + 2. Create a secondary set of testing assets that have not been copied into the the testing environment + 3. Start Asset Processor without quitonidle + 4. While Asset Processor is running add secondary set of testing assets to the testing environment + 5. Wait for Asset Processor to go idle + 6. Verify that all assets are in the cache """ + env = ap_setup_fixture level_name = "C1564064_level" new_asset = "C1564064.scriptcanvas" @@ -316,7 +358,14 @@ class TestsAssetProcessorGUI_Windows(object): def test_WindowsPlatforms_LaunchAP_LogReportsIdle(self, asset_processor, workspace, ap_idle): """ Asset Processor creates a log entry when it goes idle + + Test Steps: + 1. Create temporary testing environment + 2. Run Asset Processor batch to pre-process assets + 3. Run Asset Processor GUI + 4. Check if Asset Processor GUI reports that it has gone idle """ + asset_processor.create_temp_asset_root() # Run batch process to ensure project assets are processed assert asset_processor.batch_process(), "AP Batch failed" @@ -331,6 +380,17 @@ class TestsAssetProcessorGUI_Windows(object): @pytest.mark.assetpipeline def test_APStopTimesOut_ExceptionThrown(self, ap_setup_fixture, asset_processor): + """ + Tests whether or not Asset Processor will Time Out + + Test Steps: + 1. Create a temporary testing environment + 2. Start the Asset Processor + 3. Copy in assets to the test environment + 4. Try to stop the Asset Processor with a timeout of 1 second (This cannot be done manually). + 5. Verify that Asset Processor times out and returns the expected error + """ + asset_processor.create_temp_asset_root() asset_processor.start() @@ -347,9 +407,20 @@ class TestsAssetProcessorGUI_Windows(object): @pytest.mark.assetpipeline def test_APStopDefaultTimeout_NoException(self, asset_processor): - # If this test fails, it means other tests using the default timeout may have issues. - # In that case, either the default timeout should either be raised, or the performance - # of AP launching should be improved. + """ + Tests the default timeout of the Asset Processor + + If this test fails, it means other tests using the default timeout may have issues. + In that case, either the default timeout should either be raised, or the performance + of AP launching should be improved. + + Test Steps: + 1. Create a temporary testing environment + 2. Start the Asset Processor + 3. Stop the asset Processor without sending a timeout to it + 4. Verify that the asset processor times out and returns the expected error + """ + asset_processor.create_temp_asset_root() asset_processor.start() ap_quit_timed_out = False diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests_2.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests_2.py index b25ee081a1..3fb9ae5a81 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests_2.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_gui_tests_2.py @@ -75,10 +75,17 @@ class TestsAssetProcessorGUI_WindowsAndMac(object): @pytest.mark.test_case_id("C3540434") @pytest.mark.BAT @pytest.mark.assetpipeline - def test_WindowsAndMacPlatforms_AP_GUI_FastScanSettingCreated(self, asset_processor, fast_scan_backup): + def test_WindowsAndMacPlatforms_GUIFastScanNoSettingSet_FastScanSettingCreated(self, asset_processor, fast_scan_backup): """ Tests that a fast scan settings entry gets created for the AP if it does not exist and ensures that the entry is defaulted to fast-scan enabled + + Test Steps: + 1. Create temporary testing environment + 2. Delete existing fast scan setting if exists + 3. Run Asset Processor GUI without setting FastScan setting (default:true) and without quitonidle + 4. Wait and check to see if Windows Registry fast scan setting is created + 5. Verify that Fast Scan setting is set to true """ asset_processor.create_temp_asset_root() @@ -119,6 +126,14 @@ class TestsAssetProcessorGUI_WindowsAndMac(object): Make sure game launcher working with Asset Processor set to turbo mode Validate that no fatal errors (crashes) are reported within a certain time frame for the AP and the GameLauncher + + Test Steps: + 1. Create temporary testing environment + 2. Set fast scan to true + 3. Verify fast scan is set to true + 4. Launch game launcher + 5. Verify launcher has launched without error + 6. Verify that asset processor has launched """ CHECK_ALIVE_SECONDS = 15 @@ -166,6 +181,14 @@ class TestsAssetProcessorGUI_AllPlatforms(object): # fmt:on """ Deleting slices and uicanvases while AP is running + + Test Steps: + 1. Create temporary testing environment with test assets + 2. Launch Asset Processor and wait for it to go idle + 3. Verify product assets were created in the cache + 4. Delete test assets from the cache + 5. Wait for Asset Processor to go idle + 6. Verify product assets were regenerated in the cache """ env = ap_setup_fixture @@ -201,6 +224,15 @@ class TestsAssetProcessorGUI_AllPlatforms(object): ): """ Process slice files and uicanvas files from the additional scanfolder + + Test Steps: + 1. Create temporary testing environment + 2. Run asset processor batch + 3. Validate that product assets were generated in the cache + 4. Create an additional scan folder with assets + 5. Create additional scan folder params to pass to Asset Processor + 6. Run Asset Processor GUI with QuitOnIdle and pass in params for the additional scan folder settings + 7. Verify additional product assets from additional scan folder are present in the cache """ env = ap_setup_fixture # Copy test assets to new folder in dev folder @@ -250,6 +282,12 @@ class TestsAssetProcessorGUI_AllPlatforms(object): """ Launch AP with invalid address in bootstrap.cfg Assets should process regardless of the new address + + Test Steps: + 1. Create a temporary testing environment + 2. Set an invalid ip address in Asset Processor settings file + 3. Launch Asset Processor GUI + 4. Verify that it processes assets and exits cleanly even though it has an invalid IP. """ test_ip_address = "1.1.1.1" # an IP address without Asset Processor @@ -269,6 +307,14 @@ class TestsAssetProcessorGUI_AllPlatforms(object): def test_AllSupportedPlatforms_ModifyAssetInfo_AssetsReprocessed(self, ap_setup_fixture, asset_processor): """ Modifying assetinfo files triggers file reprocessing + + Test Steps: + 1. Create temporary testing environment with test assets + 2. Run Asset Processor GUI + 3. Verify that Asset Processor exited cleanly and product assets are in the cache + 4. Modify the .assetinfo file by adding a newline + 5. Wait for Asset Processor to go idle + 6. Verify that product files were regenerated (Time Stamp compare) """ env = ap_setup_fixture diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_relocator_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_relocator_tests.py index 2d3872bf31..30044fa9e2 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_relocator_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_relocator_tests.py @@ -85,6 +85,18 @@ class TestsAssetRelocator_WindowsAndMac(object): def test_WindowsMacPlatforms_RelocatorMoveFileWithConfirm_MoveSuccess(self, request, workspace, asset_processor, ap_setup_fixture, testId, readonly, confirm, success): + """ + Tests whether tests with Move File Confirm are successful + + Test Steps: + 1. Create temporary testing environment + 2. Set move location + 3. Determine if confirm flag is set + 4. Attempt to move the files + 5. If confirm flag set: + * Validate Move was successful + * Else: Validate move was not successful + """ env = ap_setup_fixture copied_asset = '' @@ -141,6 +153,11 @@ class TestsAssetRelocator_WindowsAndMac(object): User should be warned that LeaveEmptyFolders needs to be used with the move or delete command :return: None + + Test Steps: + 1. Create temporary testing environment + 2. Attempt to move with --LeaveEmptyFolders set + 3. Verify user is given a message that command requires to be used with --move or --delete """ env = ap_setup_fixture expected_message = "Command --leaveEmptyFolders must be used with command --move or --delete" @@ -162,6 +179,11 @@ class TestsAssetRelocator_WindowsAndMac(object): Asset with UUID/AssetId reference in non-standard format is successfully scanned and relocated to the MoveOutput folder. This test uses a pre-corrupted .slice file. + + Test Steps: + 1. Create temporary testing environment with a corrupted slice + 2. Attempt to move the corrupted slice + 3. Verify that corrupted slice was moved successfully """ env = ap_setup_fixture @@ -194,6 +216,11 @@ class TestsAssetRelocator_WindowsAndMac(object): def test_WindowsMacPlatforms_UpdateReferences_MoveCommandMessage(self, ap_setup_fixture, asset_processor): """ UpdateReferences without move or delete + + Test Steps: + 1. Create temporary testing environment + 2. Attempt to move with UpdateReferences but without move or delete flags + 3. Verify that message is returned to the user that additional flags are required """ env = ap_setup_fixture expected_message = "Command --updateReferences must be used with command --move" @@ -215,6 +242,11 @@ class TestsAssetRelocator_WindowsAndMac(object): """ When running the relocator command --AllowBrokenDependencies without the move or delete flags, the user should be warned that the flags are necessary for the functionality to be used + + Test Steps: + 1. Create temporary testing environment + 2. Attempt to move with AllowBrokenDependencies without the move or delete flag + 3. Verify that message is returned to the user that additional flags are required """ env = ap_setup_fixture @@ -302,10 +334,19 @@ class TestsAssetRelocator_WindowsAndMac(object): project ): """ + Dynamic data test for deleting a file with Asset Relocator: + C21968355 Delete a file with confirm C21968356 Delete a file without confirm C21968359 Delete a file that is marked as ReadOnly C21968360 Delete a file that is not marked as ReadOnly + + Test Steps: + 1. Create temporary testing environment + 2. Set the read-only status of the file based on the test case + 3. Run asset relocator with --delete and the confirm status based on the test case + 4. Assert file existence or nonexistence based on the test case + 5. Validate the relocation report based on expected and unexpected messages """ env = ap_setup_fixture test_file = "testFile.txt" @@ -430,6 +471,15 @@ class TestsAssetRelocator_WindowsAndMac(object): Test the LeaveEmptyFolders flag in various configurations :returns: None + + Test Steps: + 1. Create temporary testing environment + 2. Build the various move/delete commands here based on test data + 3. Run the move command with the various triggers based on test data + 4. Verify the original assets folder still exists based on test data + 5. Verify the files successfully moved to new location based on test data + 6. Verify that the files were removed from original location based on test data + 7. Verify the files have not been deleted or moved from original location based on test data """ # # Start test setup # # env = ap_setup_fixture @@ -517,6 +567,12 @@ class TestsAssetRelocator_WindowsAndMac(object): """ The test will attempt to move test assets that are not tracked under P4 source control using the EnableSCM flag Because the files are not tracked by source control, the relocation should fail + + Test Steps: + 1. Create temporary testing environment + 2. Set ReadOnly or Not-ReadOnly for the test files based on test data + 3. Generate and run the enableSCM command + 4. Verify the move failed and expected messages are present """ # Move the test assets into the project folder env = ap_setup_fixture @@ -1037,6 +1093,13 @@ class TestsAssetRelocator_WindowsAndMac(object): C21968370 AllowBrokenDependencies with move and confirm C21968371 AllowBrokenDependencies with move and without confirm C21968375 AllowBrokenDependencies with delete + + Test Steps: + 1. Create temporary testing environment + 2. Run Asset Processor to Process Assets + 3. Build primary AP Batch parameter value and destination paths + 4. Validate resulting file paths in source and output directories + 5. Validate the log based on expected and unexpected messages """ env = ap_setup_fixture all_test_asset_rel_paths = [ @@ -1254,6 +1317,18 @@ class TestsAssetRelocator_WindowsAndMac(object): @pytest.mark.parametrize("test", tests) def test_WindowsAndMac_MoveMetadataFiles_PathExistenceAndMessage(self, workspace, request, ap_setup_fixture, asset_processor, test): + """ + Tests whether moving metadata files can be moved + + Test Steps: + 1. Create temporary testing environment + 2. Determine if using wildcards on paths or not + 3. Determine if excludeMetaDataFiles is set or not + 4. Build primary AP Batch parameter value and destination paths + 5. Build and run the AP Batch command with parameters + 6. Validate resulting file paths in source and output directories + 7. Validate the log based on expected and unexpected messages + """ env = ap_setup_fixture def teardown(): @@ -1342,7 +1417,7 @@ class TestsAssetRelocator_WindowsAndMac(object): @dataclass class MoveTest: - description: str # test case title directly copied from Testrail + description: str # test case title asset_folder: str # which folder in ./assets will be used for this test encoded_command: str # the command to execute encoded_output_dir: str # the destination directory to validate @@ -1350,7 +1425,7 @@ class MoveTest: name_change_map: dict = None files_that_stay: List[str] = field(default_factory=lambda: []) output_messages: List[str] = field(default_factory=lambda: []) - step: str = None # the step of the test from Testrail + step: str = None # the step of the test from test repository prefix_commands: List[str] = field(default_factory=lambda: ["AssetProcessorBatch", "--zeroAnalysisMode"]) suffix_commands: List[str] = field(default_factory=lambda: ["--confirm"]) env: dict = field(init=False, default=None) # inject the ap_setup_fixture at runtime @@ -3718,7 +3793,18 @@ class TestsAssetProcessorMove_WindowsAndMac: # -k C19462747 @pytest.mark.parametrize("test", move_a_file_tests + move_a_folder_tests) - def test_WindowsMacPlatforms_MoveCommand(self, asset_processor, ap_setup_fixture, test: MoveTest, project): + def test_WindowsMacPlatforms_MoveCommand_CommandResult(self, asset_processor, ap_setup_fixture, test: MoveTest, project): + """ + + Test Steps: + 1. Create temporary testing environment based on test data + 2. Validate that temporary testing environment was created successfully + 3. Execute the move command based upon the test data + 4. Validate that files are where they're expected according to the test data + 5. Validate unexpected files are not found according to the test data + 6. Validate output messages according to the test data + 7. Validate move status according to the test data + """ source_folder, _ = asset_processor.prepare_test_environment(ap_setup_fixture["tests_dir"], test.asset_folder) test.map_env(ap_setup_fixture, source_folder) diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/missing_dependency_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/missing_dependency_tests.py index 432b6cdfc8..74f6de1129 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/missing_dependency_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/missing_dependency_tests.py @@ -75,6 +75,15 @@ class TestsMissingDependencies_WindowsAndMac(object): def do_missing_dependency_test(self, source_product, expected_dependencies, dsp_param, platforms=None, max_iterations=0): + """ + Test Steps: + 1. Determine what platforms to run against + 2. Process assets for that platform + 3. Determine the missing dependency params to set + 4. Set the max iteration param + 5. Run missing dependency scanner against target platforms and search params based on test data + 6. Validate missing dependencies against test data + """ platforms = platforms or ASSET_PROCESSOR_PLATFORM_MAP[self._workspace.asset_processor_platform] if not isinstance(platforms, list): @@ -104,7 +113,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_ValidUUIDNotDependency_ReportsMissingDependency(self): - """Tests that a valid UUID referenced in a file will report any missing dependencies""" + """ + Tests that a valid UUID referenced in a file will report any missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with missing dependencies expected_product = f"testassets\\validuuidsnotdependency.txt" @@ -141,7 +157,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_InvalidUUIDsNotDependencies_NoReportedMessage(self): - """Tests that invalid UUIDs do not count as missing dependencies""" + """ + Tests that invalid UUIDs do not count as missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with invalid UUIDs expected_product = f"testassets\\invaliduuidnoreport.txt" expected_dependencies = [] # No expected missing dependencies @@ -153,7 +176,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_ValidAssetIdsNotDependencies_ReportsMissingDependency(self): - """Tests that valid asset IDs but not dependencies, show missing dependencies""" + """ + Tests that valid asset IDs but not dependencies, show missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with valid asset ids but not dependencies expected_product = f"testassets\\validassetidnotdependency.txt" @@ -173,7 +203,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_InvalidAssetsIDNotDependencies_NoReportedMessage(self): - """Tests that invalid asset IDs do not count as missing dependencies""" + """ + Tests that invalid asset IDs do not count as missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with invalid asset IDs expected_product = f"testassets\\invalidassetidnoreport.txt" @@ -188,7 +225,14 @@ class TestsMissingDependencies_WindowsAndMac(object): # fmt:off def test_WindowsAndMac_ValidSourcePathsNotDependencies_ReportsMissingDependencies(self): # fmt:on - """Tests that valid source paths can translate to missing dependencies""" + """ + Tests that valid source paths can translate to missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with missing dependencies as source paths expected_product = f"testassets\\relativesourcepathsnotdependencies.txt" @@ -212,7 +256,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_InvalidARelativePathsNotDependencies_NoReportedMessage(self): - """Tests that invalid relative paths do not resolve to missing dependencies""" + """ + Tests that invalid relative paths do not resolve to missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to the txt file with invalid relative paths expected_product = f"testassets\\invalidrelativepathsnoreport.txt" @@ -227,7 +278,14 @@ class TestsMissingDependencies_WindowsAndMac(object): # fmt:off def test_WindowsAndMac_ValidProductPathsNotDependencies_ReportsMissingDependencies(self): # fmt:on - """Tests that valid product paths can resolve to missing dependencies""" + """ + Tests that valid product paths can resolve to missing dependencies + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ self._asset_processor.add_source_folder_assets(f"Gems\\LyShineExamples\\Assets\\UI\\Fonts\\LyShineExamples") self._asset_processor.add_scan_folder(f"Gems\\LyShineExamples\\Assets") @@ -260,7 +318,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_WildcardScan_FindsAllExpectedFiles(self): - """Tests that the wildcard scanning will pick up multiple files""" + """ + Tests that the wildcard scanning will pick up multiple files + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ helper = self._missing_dep_helper @@ -291,6 +356,11 @@ class TestsMissingDependencies_WindowsAndMac(object): For these references that are valid, all but one have available, matching dependencies. This test is primarily meant to verify that the missing dependency reporter checks the product dependency table before emitting missing dependencies. + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test """ # Relative path to target test file expected_product = f"testassets\\reportonemissingdependency.txt" @@ -305,7 +375,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_ReferencesSelfPath_NoReportedMessage(self): - """Tests that a file that references itself via relative path does not report itself as a missing dependency""" + """ + Tests that a file that references itself via relative path does not report itself as a missing dependency + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to file that references itself via relative path expected_product = f"testassets\\selfreferencepath.txt" expected_dependencies = [] @@ -317,7 +394,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_ReferencesSelfUUID_NoReportedMessage(self): - """Tests that a file that references itself via its UUID does not report itself as a missing dependency""" + """ + Tests that a file that references itself via its UUID does not report itself as a missing dependency + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to file that references itself via its UUID expected_product = f"testassets\\selfreferenceuuid.txt" @@ -330,7 +414,14 @@ class TestsMissingDependencies_WindowsAndMac(object): @pytest.mark.assetpipeline @pytest.mark.test_case_id("C17226567") def test_WindowsAndMac_ReferencesSelfAssetID_NoReportedMessage(self): - """Tests that a file that references itself via its Asset ID does not report itself as a missing dependency""" + """ + Tests that a file that references itself via its Asset ID does not report itself as a missing dependency + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to file that references itself via its Asset ID expected_product = f"testassets\\selfreferenceassetid.txt" @@ -347,6 +438,11 @@ class TestsMissingDependencies_WindowsAndMac(object): Tests that the scan limit fails to find a missing dependency that is out of reach. The max iteration count is set to just under where a valid missing dependency is on a line in the file, so this will not report any missing dependencies. + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test """ # Relative path to file that has a missing dependency at 31 iterations deep @@ -364,7 +460,13 @@ class TestsMissingDependencies_WindowsAndMac(object): Tests that the scan limit succeeds in finding a missing dependency that is barely in reach. In the previous test, the scanner was set to stop recursion just before a missing dependency was found. This test runs with the recursion limit set deep enough to actually find the missing dependency. + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test """ + # Relative path to file that has a missing dependency at 31 iterations deep expected_product = f"testassets\\maxiteration31deep.txt" @@ -383,7 +485,14 @@ class TestsMissingDependencies_WindowsAndMac(object): # fmt:off def test_WindowsAndMac_PotentialMatchesLongerThanUUIDString_OnlyReportsCorrectLengthUUIDs(self): # fmt:on - """Tests that dependency references that are longer than expected are ignored""" + """ + Tests that dependency references that are longer than expected are ignored + + Test Steps: + 1. Set the expected product + 2. Set the expected missing dependencies + 3. Execute test + """ # Relative path to text file with varying length UUID references expected_product = f"testassets\\onlymatchescorrectlengthuuids.txt" @@ -408,7 +517,14 @@ class TestsMissingDependencies_WindowsAndMac(object): def test_WindowsAndMac_MissingDependencyScanner_GradImageSuccess( self, ap_setup_fixture ): - """Tests the Missing Dependency Scanner can scan gradimage files""" + """ + Tests the Missing Dependency Scanner can scan gradimage files + + Test Steps: + 1. Create temporary testing environment + 2. Run the move dependency scanner against the gradimage + 2. Validate that the expected product files and and expected depdencies match + """ env = ap_setup_fixture helper = self._missing_dep_helper diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/auxiliary_content_tests/auxiliary_content_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/auxiliary_content_tests/auxiliary_content_tests.py index 7e9f65de60..452dc66352 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/auxiliary_content_tests/auxiliary_content_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/auxiliary_content_tests/auxiliary_content_tests.py @@ -51,6 +51,11 @@ class TestAuxiliaryContent: def test_CreateAuxiliaryContent_DontSkipLevelPaks(self, workspace, level): """ This test ensure that Auxiliary Content contain level.pak files + + Test Steps: + 1. Run auxiliary content against project under test + 2. Validate auxiliary content exists + 3. Verifies that level.pak exists """ path_to_dev = workspace.paths.engine_root() @@ -70,6 +75,11 @@ class TestAuxiliaryContent: def test_CreateAuxiliaryContent_SkipLevelPaks(self, workspace, level): """ This test ensure that Auxiliary Content contain no level.pak file + + Test Steps: + 1. Run auxiliary content against project under test with skiplevelPaks flag + 2. Validate auxiliary content exists + 3. Validate level.pak was added to auxiliary content """ path_to_dev = workspace.paths.engine_root() diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py index b66984666b..afd7190c1b 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/fbx_tests.py @@ -533,6 +533,14 @@ class TestsFBX_AllPlatforms(object): def test_FBXBlackboxTest_SourceFiles_Processed_ResultInExpectedProducts(self, workspace, ap_setup_fixture, asset_processor, project, blackbox_param): + """ + Please see run_fbx_test(...) for details + + Test Steps: + 1. Determine if blackbox is set to none + 2. Run FBX Test + """ + if blackbox_param == None: return self.run_fbx_test(workspace, ap_setup_fixture, @@ -544,6 +552,15 @@ class TestsFBX_AllPlatforms(object): workspace, ap_setup_fixture, asset_processor, project, blackbox_param): + """ + Please see run_fbx_test(...) for details + + Test Steps: + 1. Determine if blackbox is set to none + 2. Run FBX Test + 2. Re-run FBX test and validate the information in override assets + """ + if blackbox_param == None: return self.run_fbx_test(workspace, ap_setup_fixture, @@ -567,6 +584,19 @@ class TestsFBX_AllPlatforms(object): def run_fbx_test(self, workspace, ap_setup_fixture, asset_processor, project, blackbox_params: BlackboxAssetTest, overrideAsset = False): + """ + These tests work by having the test case ingest the test data and determine the run pattern. + Tests will process scene settings files and will additionally do a verification against a provided debug file + Additionally, if an override is passed, the output is checked against the override. + + Test Steps: + 1. Create temporary test environment + 2. Process Assets + 3. Determine what assets to validate based upon test data + 4. Validate assets were created in cache + 5. If debug file provided, verify scene files were generated correctly + 6. Verify that each given source asset resulted in the expected jobs and products + """ test_assets_folder = blackbox_params.override_asset_folder if overrideAsset else blackbox_params.asset_folder logger.info(f"{blackbox_params.test_name}: Processing assets in folder '" diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/wwise_bank_dependency_tests/bank_info_parser_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/wwise_bank_dependency_tests/bank_info_parser_tests.py index 0db13bf53c..764b7723bf 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/wwise_bank_dependency_tests/bank_info_parser_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/wwise_bank_dependency_tests/bank_info_parser_tests.py @@ -26,6 +26,18 @@ def soundbank_metadata_generator_setup_fixture(workspace): def success_case_test(test_folder, expected_dependencies_dict, bank_info, expected_result_code=0): + """ + Test Steps: + 1. Make sure the return code is what was expected, and that the expected number of banks were returned. + 2. Validate bank is in the expected dependencies dictionary. + 3. Validate the path to output the metadata file to was assembled correctly. + 4. Validate metadata object for this bank is set, and that it has an object assigned to its dependencies field + and its includedEvents field + 5. Validate metadata object has the correct number of dependencies, and validated that every expected dependency + exists in the dependencies list of the metadata object. + 6. Validate metadata object has the correct number of events, and validate that every expected event exists in the + events of the metadata object. + """ expected_bank_count = len(expected_dependencies_dict) banks, result_code = bank_info.generate_metadata( @@ -80,8 +92,17 @@ class TestSoundBankMetadataGenerator: def test_NoMetadataTooFewBanks_ReturnCodeIsError(self, workspace, soundbank_metadata_generator_setup_fixture): - # Trying to generate metadata for banks in a folder with one or fewer banks and no metadata is not possible - # and should fail. + """ + Trying to generate metadata for banks in a folder with one or fewer banks and no metadata is not possible + and should fail. + + Test Steps: + 1. Setup testing environment with only 1 bank file + 2. Get Sound Bank Info + 3. Attempt to generate sound bank metadata + 4. Verify that proper error code is returned + """ + # test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_NoMetadataTooFewBanks_ReturnCodeIsError') if not os.path.isdir(test_assets_folder): @@ -97,15 +118,30 @@ class TestSoundBankMetadataGenerator: assert error_code is 2, 'Metadata was generated when there were fewer than two banks in the target directory.' def test_NoMetadataNoContentBank_NoMetadataGenerated(self, workspace, soundbank_metadata_generator_setup_fixture): + """ + Test Steps: + 1. Setup testing environment + 2. No expected dependencies + 3. Call success case test + """ test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_NoMetadataNoContentBank_NoMetadataGenerated') expected_dependencies = dict() success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_NoMetadataOneContentBank_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture): - # When no Wwise metadata is present, and there is only one content bank in the target directory with no wem - # files, then only the content bank should have metadata associated with it. The generated metadata should - # only describe a dependency on the init bank. + """ + When no Wwise metadata is present, and there is only one content bank in the target directory with no wem + files, then only the content bank should have metadata associated with it. The generated metadata should + only describe a dependency on the init bank. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_NoMetadataOneContentBank_NoStreamedFiles_OneDependency') @@ -116,9 +152,18 @@ class TestSoundBankMetadataGenerator: def test_NoMetadataOneContentBank_StreamedFiles_MultipleDependencies(self, workspace, soundbank_metadata_generator_setup_fixture): - # When no Wwise metadata is present, and there is only one content bank in the target directory with wem files - # present, then only the content bank should have metadata associated with it. The generated metadata should - # describe a dependency on the init bank and all wem files in the folder. + """ + When no Wwise metadata is present, and there is only one content bank in the target directory with wem files + present, then only the content bank should have metadata associated with it. The generated metadata should + describe a dependency on the init bank and all wem files in the folder. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_NoMetadataOneContentBank_StreamedFiles_MultipleDependencies') @@ -136,10 +181,19 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_NoMetadataMultipleBanks_OneDependency_ReturnCodeIsWarning(self, workspace, soundbank_metadata_generator_setup_fixture): - # When no Wwise metadata is present, and there are multiple content banks in the target directory with wem files - # present, there is no way to tell which bank requires which wem files. A warning should be emitted, - # stating that the full dependency graph could not be created, and only dependencies on the init bank are - # described in the generated metadata files. + """ + When no Wwise metadata is present, and there are multiple content banks in the target directory with wem files + present, there is no way to tell which bank requires which wem files. A warning should be emitted, + stating that the full dependency graph could not be created, and only dependencies on the init bank are + described in the generated metadata files. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_NoMetadataMultipleBanks_OneDependency_ReturnCodeIsWarning') bank_info = get_bank_info(workspace) @@ -150,8 +204,17 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace), expected_result_code=1) def test_OneContentBank_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes one content bank that contains all media needed by its events. Generated metadata - # describes a dependency only on the init bank. + """ + Wwise metadata describes one content bank that contains all media needed by its events. Generated metadata + describes a dependency only on the init bank. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_OneContentBank_NoStreamedFiles_OneDependency') @@ -165,8 +228,17 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_OneContentBank_StreamedFiles_MultipleDependencies(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes one content bank that references streamed media files needed by its events. Generated - # metadata describes dependencies on the init bank and wems named by the IDs of referenced streamed media. + """ + Wwise metadata describes one content bank that references streamed media files needed by its events. Generated + metadata describes dependencies on the init bank and wems named by the IDs of referenced streamed media. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_OneContentBank_StreamedFiles_MultipleDependencies') @@ -187,8 +259,17 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_NoStreamedFiles_OneDependency(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Each bank contains all media needed by its events. Generated - # metadata describes each bank having a dependency only on the init bank. + """ + Wwise metadata describes multiple content banks. Each bank contains all media needed by its events. Generated + metadata describes each bank having a dependency only on the init bank. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_NoStreamedFiles_OneDependency') @@ -206,8 +287,17 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_Bank1StreamedFiles(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events, - # while bank 2 contains all media need by its events. + """ + Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events, + while bank 2 contains all media need by its events. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_Bank1StreamedFiles') @@ -228,9 +318,18 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_SplitBanks_OnlyBankDependenices(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Bank 3 events require media that is contained in bank 4. - # Generated metadata describes each bank having a dependency on the init bank, while bank 3 has an additional - # dependency on bank 4. + """ + Wwise metadata describes multiple content banks. Bank 3 events require media that is contained in bank 4. + Generated metadata describes each bank having a dependency on the init bank, while bank 3 has an additional + dependency on bank 4. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_SplitBanks_OnlyBankDependenices') @@ -248,9 +347,18 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_ReferencedEvent_MediaEmbeddedInBank(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Bank 1 contains all media required by its events, while bank - # 5 contains a reference to an event in bank 1, but no media for that event. Generated metadata describes both - # banks having a dependency on the init bank, while bank 5 has an additional dependency on bank 1. + """ + Wwise metadata describes multiple content banks. Bank 1 contains all media required by its events, while bank + 5 contains a reference to an event in bank 1, but no media for that event. Generated metadata describes both + banks having a dependency on the init bank, while bank 5 has an additional dependency on bank 1. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_ReferencedEvent_MediaEmbeddedInBank') @@ -271,10 +379,19 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_ReferencedEvent_MediaStreamed(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events, - # while bank 5 contains a reference to an event in bank 1. This causes bank 5 to also describe a reference to - # the streamed media file referenced by the event from bank 1. Generated metadata describes both banks having - # dependencies on the init bank, as well as the wem named by the ID of referenced streamed media. + """ + Wwise metadata describes multiple content banks. Bank 1 references streamed media files needed by its events, + while bank 5 contains a reference to an event in bank 1. This causes bank 5 to also describe a reference to + the streamed media file referenced by the event from bank 1. Generated metadata describes both banks having + dependencies on the init bank, as well as the wem named by the ID of referenced streamed media. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_ReferencedEvent_MediaStreamed') @@ -298,11 +415,20 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_ReferencedEvent_MixedSources(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks. Bank 1 references a streamed media files needed by one of its - # events, and contains all media needed for its other events, while bank 5 contains a reference to two events - # in bank 1: one that requires streamed media, and one that requires media embedded in bank 1. Generated - # metadata describes both banks having dependencies on the init bank and the wem named by the ID of referenced - # streamed media, while bank 5 has an additional dependency on bank 1. + """ + Wwise metadata describes multiple content banks. Bank 1 references a streamed media files needed by one of its + events, and contains all media needed for its other events, while bank 5 contains a reference to two events + in bank 1: one that requires streamed media, and one that requires media embedded in bank 1. Generated + metadata describes both banks having dependencies on the init bank and the wem named by the ID of referenced + streamed media, while bank 5 has an additional dependency on bank 1. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_ReferencedEvent_MixedSources') @@ -332,8 +458,17 @@ class TestSoundBankMetadataGenerator: success_case_test(test_assets_folder, expected_dependencies, get_bank_info(workspace)) def test_MultipleContentBanks_VaryingDependencies_MixedSources(self, workspace, soundbank_metadata_generator_setup_fixture): - # Wwise metadata describes multiple content banks that have varying dependencies on each other, and dependencies - # on streamed media files. + """ + Wwise metadata describes multiple content banks that have varying dependencies on each other, and dependencies + on streamed media files. + + Test Steps: + 1. Setup testing environment + 2. Get current bank info + 3. Build expected dependencies + 4. Call success case test + """ + test_assets_folder = os.path.join(soundbank_metadata_generator_setup_fixture['tests_dir'], 'assets', 'test_MultipleContentBanks_VaryingDependencies_MixedSources') From 7ee55cce3a858ca5dae0fd1acfb130f9a468cd75 Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:19:03 -0700 Subject: [PATCH 38/53] Post merge fixes for spawning entities --- .../ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp index 21d1fc5264..ad53236108 100644 --- a/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp +++ b/Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Spawning/SpawnNodeable.cpp @@ -105,7 +105,7 @@ namespace ScriptCanvas::Nodeables::Spawning return; } - auto preSpawnCB = [this, translation, rotation, scale]([[maybe_unused]] AzFramework::EntitySpawnTicket& ticket, + auto preSpawnCB = [this, translation, rotation, scale]([[maybe_unused]] AzFramework::EntitySpawnTicket::Id ticketId, AzFramework::SpawnableEntityContainerView view) { AZ::Entity* rootEntity = *view.begin(); @@ -122,7 +122,7 @@ namespace ScriptCanvas::Nodeables::Spawning } }; - auto spawnCompleteCB = [this]([[maybe_unused]] AzFramework::EntitySpawnTicket& ticket, + auto spawnCompleteCB = [this]([[maybe_unused]] AzFramework::EntitySpawnTicket::Id ticketId, AzFramework::SpawnableConstEntityContainerView view) { AZStd::lock_guard lock(m_idBatchMutex); @@ -134,6 +134,7 @@ namespace ScriptCanvas::Nodeables::Spawning m_spawnBatchSizes.push_back(view.size()); }; - AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_spawnTicket, preSpawnCB, spawnCompleteCB); + AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities( + m_spawnTicket, AzFramework::SpawnablePriority_Default, preSpawnCB, spawnCompleteCB); } } From 50277cc17838eb832e6392491a11acab34380ae0 Mon Sep 17 00:00:00 2001 From: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:31:21 -0700 Subject: [PATCH 39/53] LYN-4132 Disable SIMD exceptions in profile (#1052) * Fix old method call * Disable SIMD exceptions in profile --- Code/CryEngine/CrySystem/SystemInit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Code/CryEngine/CrySystem/SystemInit.cpp b/Code/CryEngine/CrySystem/SystemInit.cpp index 79041fa233..52744519bb 100644 --- a/Code/CryEngine/CrySystem/SystemInit.cpp +++ b/Code/CryEngine/CrySystem/SystemInit.cpp @@ -2017,8 +2017,8 @@ void CSystem::CreateSystemVars() REGISTER_CVAR2("sys_streaming_in_blocks", &g_cvars.sys_streaming_in_blocks, 1, VF_NULL, "Streaming of large files happens in blocks"); -#if (defined(WIN32) || defined(WIN64)) && !defined(_RELEASE) - REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 3, 0, "Use or not use floating point exceptions."); +#if (defined(WIN32) || defined(WIN64)) && defined(_DEBUG) + REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 2, 0, "Use or not use floating point exceptions."); #else // Float exceptions by default disabled for console builds. REGISTER_CVAR2("sys_float_exceptions", &g_cvars.sys_float_exceptions, 0, 0, "Use or not use floating point exceptions."); #endif From 087677b3266d48270c0419bd19352cfc2bf8d3e1 Mon Sep 17 00:00:00 2001 From: Nicholas Van Sickle Date: Tue, 1 Jun 2021 12:20:15 -0700 Subject: [PATCH 40/53] Fix several viewport issues (#1045) * Fix some FOV calculation viewport issues: -Avoid calculating FOV if we've got an invalid viewport -Don't override game mode FOV, let the active camera components manage it instead * Fix viewport font positioning This updates code in a few places to respect an API change/fix made to AtomFont - also switched the default value of m_virtual800x600ScreenSize to false as it's really behavior you want to opt into * Don't activate CameraComponentController when in the Editor / not in game mode --- .../AzFramework/Font/FontInterface.h | 2 +- Code/Sandbox/Editor/EditorViewportWidget.cpp | 34 ++++++++++++------- .../AtomDebugDisplayViewportInterface.cpp | 4 +-- ...AtomViewportDisplayInfoSystemComponent.cpp | 2 +- .../Code/Source/CameraComponentController.cpp | 4 ++- .../Code/Source/EditorCameraComponent.cpp | 11 ------ 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Font/FontInterface.h b/Code/Framework/AzFramework/AzFramework/Font/FontInterface.h index 04a0572bb9..dae5fa9fe7 100644 --- a/Code/Framework/AzFramework/AzFramework/Font/FontInterface.h +++ b/Code/Framework/AzFramework/AzFramework/Font/FontInterface.h @@ -54,7 +54,7 @@ namespace AzFramework AZ::Matrix3x4 m_transform = AZ::Matrix3x4::Identity(); //!< Transform to apply to text quads bool m_monospace = false; //!< disable character proportional spacing bool m_depthTest = false; //!< Test character against the depth buffer - bool m_virtual800x600ScreenSize = true; //!< Text placement and size are scaled relative to a virtual 800x600 resolution + bool m_virtual800x600ScreenSize = false; //!< Text placement and size are scaled relative to a virtual 800x600 resolution bool m_scaleWithWindow = false; //!< Font gets bigger as the window gets bigger bool m_multiline = true; //!< text respects ascii newline characters }; diff --git a/Code/Sandbox/Editor/EditorViewportWidget.cpp b/Code/Sandbox/Editor/EditorViewportWidget.cpp index 989d6e407d..24d1590808 100644 --- a/Code/Sandbox/Editor/EditorViewportWidget.cpp +++ b/Code/Sandbox/Editor/EditorViewportWidget.cpp @@ -463,16 +463,20 @@ void EditorViewportWidget::Update() m_renderViewport->GetViewportContext()->SetCameraTransform(LYTransformToAZTransform(m_Camera.GetMatrix())); } - AZ::Matrix4x4 clipMatrix; - AZ::MakePerspectiveFovMatrixRH( - clipMatrix, - m_Camera.GetFov(), - aznumeric_cast(width()) / aznumeric_cast(height()), - m_Camera.GetNearPlane(), - m_Camera.GetFarPlane(), - true - ); - m_renderViewport->GetViewportContext()->SetCameraProjectionMatrix(clipMatrix); + // Don't override the game mode FOV + if (!GetIEditor()->IsInGameMode()) + { + AZ::Matrix4x4 clipMatrix; + AZ::MakePerspectiveFovMatrixRH( + clipMatrix, + GetFOV(), + aznumeric_cast(width()) / aznumeric_cast(height()), + m_Camera.GetNearPlane(), + m_Camera.GetFarPlane(), + true + ); + m_renderViewport->GetViewportContext()->SetCameraProjectionMatrix(clipMatrix); + } m_updatingCameraPosition = false; @@ -870,6 +874,13 @@ void EditorViewportWidget::OnBeginPrepareRender() int w = m_rcClient.width(); int h = m_rcClient.height(); + // Don't bother doing an FOV calculation if we don't have a valid viewport + // This prevents frustum calculation bugs with a null viewport + if (w <= 1 || h <= 1) + { + return; + } + float fov = gSettings.viewports.fDefaultFov; // match viewport fov to default / selected title menu fov @@ -1782,9 +1793,6 @@ void EditorViewportWidget::SetViewTM(const Matrix34& viewTM, bool bMoveOnly) cameraObject->SetWorldTM(camMatrix * AZMatrix3x3ToLYMatrix3x3(lookThroughEntityCorrection)); } } - - using namespace AzToolsFramework; - ComponentEntityObjectRequestBus::Event(cameraObject, &ComponentEntityObjectRequestBus::Events::UpdatePreemptiveUndoCache); } else if (m_viewEntityId.IsValid()) { diff --git a/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp b/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp index 6c68618f78..620d5d1fb8 100644 --- a/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp +++ b/Gems/AtomLyIntegration/AtomBridge/Code/Source/AtomDebugDisplayViewportInterface.cpp @@ -1328,7 +1328,7 @@ namespace AZ::AtomBridge params.m_hAlign = center ? AzFramework::TextHorizontalAlignment::Center : AzFramework::TextHorizontalAlignment::Left; //! Horizontal text alignment params.m_monospace = false; //! disable character proportional spacing params.m_depthTest = false; //! Test character against the depth buffer - params.m_virtual800x600ScreenSize = true; //! Text placement and size are scaled relative to a virtual 800x600 resolution + params.m_virtual800x600ScreenSize = false; //! Text placement and size are scaled in viewport pixel coordinates params.m_scaleWithWindow = false; //! Font gets bigger as the window gets bigger params.m_multiline = true; //! text respects ascii newline characters @@ -1364,7 +1364,7 @@ namespace AZ::AtomBridge params.m_hAlign = center ? AzFramework::TextHorizontalAlignment::Center : AzFramework::TextHorizontalAlignment::Left; //! Horizontal text alignment params.m_monospace = false; //! disable character proportional spacing params.m_depthTest = false; //! Test character against the depth buffer - params.m_virtual800x600ScreenSize = true; //! Text placement and size are scaled relative to a virtual 800x600 resolution + params.m_virtual800x600ScreenSize = false; //! Text placement and size are scaled in viewport pixel coordinates params.m_scaleWithWindow = false; //! Font gets bigger as the window gets bigger params.m_multiline = true; //! text respects ascii newline characters diff --git a/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp b/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp index 146c0c67d0..d47ce44ab2 100644 --- a/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp +++ b/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp @@ -162,7 +162,7 @@ namespace AZ::Render m_drawParams.m_hAlign = AzFramework::TextHorizontalAlignment::Right; m_drawParams.m_monospace = false; m_drawParams.m_depthTest = false; - m_drawParams.m_virtual800x600ScreenSize = true; + m_drawParams.m_virtual800x600ScreenSize = false; m_drawParams.m_scaleWithWindow = false; m_drawParams.m_multiline = true; m_drawParams.m_lineSpacing = 0.5f; diff --git a/Gems/Camera/Code/Source/CameraComponentController.cpp b/Gems/Camera/Code/Source/CameraComponentController.cpp index 3dcee68169..d0a124067b 100644 --- a/Gems/Camera/Code/Source/CameraComponentController.cpp +++ b/Gems/Camera/Code/Source/CameraComponentController.cpp @@ -240,7 +240,9 @@ namespace Camera CameraBus::Handler::BusConnect(); CameraNotificationBus::Broadcast(&CameraNotificationBus::Events::OnCameraAdded, m_entityId); - if (m_config.m_makeActiveViewOnActivation) + // Activate our camera if we're running from the launcher or Editor game mode + // Otherwise, let the Editor keep managing the active camera + if (m_config.m_makeActiveViewOnActivation && (!gEnv || !gEnv->IsEditor() || gEnv->IsEditorGameMode())) { MakeActiveView(); } diff --git a/Gems/Camera/Code/Source/EditorCameraComponent.cpp b/Gems/Camera/Code/Source/EditorCameraComponent.cpp index 359fa936bc..80057e7a77 100644 --- a/Gems/Camera/Code/Source/EditorCameraComponent.cpp +++ b/Gems/Camera/Code/Source/EditorCameraComponent.cpp @@ -34,20 +34,9 @@ namespace Camera auto controllerConfig = m_controller.GetConfiguration(); controllerConfig.m_editorEntityId = GetEntityId().operator AZ::u64(); - // The Editor manages active camera state, so while we're in Editor we explicitly - // disable the request to make this the active view at edit component activation time. - bool prevShouldActivateViewOnActivation = controllerConfig.m_makeActiveViewOnActivation; - controllerConfig.m_makeActiveViewOnActivation = false; - - m_controller.SetConfiguration(controllerConfig); - // Call base class activate, which in turn calls Activate on our controller. EditorCameraComponentBase::Activate(); - // Reset the original `m_makeActiveViewOnActivation' setting, so that the intended value is serialized, used in BuildGameEntity, etc. - controllerConfig.m_makeActiveViewOnActivation = prevShouldActivateViewOnActivation; - m_controller.SetConfiguration(controllerConfig); - AzFramework::EntityDebugDisplayEventBus::Handler::BusConnect(GetEntityId()); EditorCameraNotificationBus::Handler::BusConnect(); EditorCameraViewRequestBus::Handler::BusConnect(GetEntityId()); From 940439d247ee7a46cf6f7a4591ce5a3857e1c2e7 Mon Sep 17 00:00:00 2001 From: Terry Michaels Date: Tue, 1 Jun 2021 14:25:38 -0500 Subject: [PATCH 41/53] Resaved Simple Level to remove deleted components (#1066) --- AutomatedTesting/Levels/Simple/Simple.ly | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AutomatedTesting/Levels/Simple/Simple.ly b/AutomatedTesting/Levels/Simple/Simple.ly index 0148ee6e34..0a063bf8f8 100644 --- a/AutomatedTesting/Levels/Simple/Simple.ly +++ b/AutomatedTesting/Levels/Simple/Simple.ly @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:804193a2afd68cd1e6bec8155ea11400566f2941fbd6eb0c324839ebcd10192d -size 8492 +oid sha256:302d6172156e8ed665e44e206d81f54f1b0f1008d73327300ea92f8c1159780b +size 11820 From 4070a9ec303573915b2b98af0dea3e0665946970 Mon Sep 17 00:00:00 2001 From: Junbo Liang <68558268+junbo75@users.noreply.github.com> Date: Tue, 1 Jun 2021 13:57:55 -0700 Subject: [PATCH 42/53] [LYN-2243] Create end-to-end automation tests for the metrics gem (#23) [LYN-2243] Create end-to-end automation tests for the metrics gem --- .../AWS/Windows/aws_metrics/__init__.py | 10 + .../aws_metrics_automation_test.py | 237 ++++++++++++++++ .../Windows/aws_metrics/aws_metrics_utils.py | 252 ++++++++++++++++++ .../aws_metrics/aws_metrics_waiters.py | 142 ++++++++++ .../Gem/PythonTests/AWS/Windows/cdk/cdk.py | 57 +++- .../PythonTests/AWS/common/aws_credentials.py | 134 ++++++++++ .../Gem/PythonTests/AWS/common/aws_utils.py | 172 ++++++------ .../PythonTests/AWS/common/custom_waiter.py | 91 +++++++ .../Registry/awscoreconfiguration.setreg | 2 +- .../cdk/aws_metrics/batch_processing.py | 5 +- .../cdk/aws_metrics/data_ingestion.py | 4 +- 11 files changed, 1017 insertions(+), 89 deletions(-) create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/__init__.py create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_utils.py create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_waiters.py create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/common/aws_credentials.py create mode 100644 AutomatedTesting/Gem/PythonTests/AWS/common/custom_waiter.py diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/__init__.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/__init__.py new file mode 100644 index 0000000000..cdee4b5a56 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/__init__.py @@ -0,0 +1,10 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py new file mode 100644 index 0000000000..04be31759d --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py @@ -0,0 +1,237 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import logging +import os +import pytest +import time +import typing + +from datetime import datetime +import ly_test_tools.log.log_monitor + +from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor as asset_processor +from AWS.common.aws_utils import aws_utils +from AWS.common.aws_credentials import aws_credentials +from AWS.Windows.resource_mappings.resource_mappings import resource_mappings +from AWS.Windows.cdk.cdk import cdk +from .aws_metrics_utils import aws_metrics_utils + +AWS_METRICS_FEATURE_NAME = 'AWSMetrics' +GAME_LOG_NAME = 'Game.log' + +logger = logging.getLogger(__name__) + + +def setup(launcher: ly_test_tools.launchers.Launcher, + cdk: cdk, + asset_processor: asset_processor, + resource_mappings: resource_mappings, + context_variable: str = '') -> typing.Tuple[ly_test_tools.log.log_monitor.LogMonitor, str, str]: + """ + Set up the CDK application and start the log monitor. + :param launcher: Client launcher for running the test level. + :param cdk: CDK application for deploying the AWS resources. + :param asset_processor: asset_processor fixture. + :param resource_mappings: resource_mappings fixture. + :param context_variable: context_variable for enable optional CDK feature. + :return log monitor object, metrics file path and the metrics stack name. + """ + logger.info(f'Cdk stack names:\n{cdk.list()}') + stacks = cdk.deploy(context_variable=context_variable) + resource_mappings.populate_output_keys(stacks) + + asset_processor.start() + asset_processor.wait_for_idle() + + metrics_file_path = os.path.join(launcher.workspace.paths.project(), 'user', + AWS_METRICS_FEATURE_NAME, 'metrics.json') + remove_file(metrics_file_path) + + file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), GAME_LOG_NAME) + remove_file(file_to_monitor) + + # Initialize the log monitor. + log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor) + + return log_monitor, metrics_file_path, stacks[0] + + +def monitor_metrics_submission(log_monitor: ly_test_tools.log.log_monitor.LogMonitor) -> None: + """ + Monitor the messages and notifications for submitting metrics. + :param log_monitor: Log monitor to check the log messages. + """ + expected_lines = [ + '(Script) - Submitted metrics without buffer.', + '(Script) - Submitted metrics with buffer.', + '(Script) - Metrics is sent successfully.' + ] + + unexpected_lines = [ + '(Script) - Failed to submit metrics without buffer.', + '(Script) - Failed to submit metrics with buffer.', + '(Script) - Failed to send metrics.' + ] + + result = log_monitor.monitor_log_for_lines( + expected_lines=expected_lines, + unexpected_lines=unexpected_lines, + halt_on_unexpected=True) + + # Assert the log monitor detected expected lines and did not detect any unexpected lines. + assert result, ( + f'Log monitoring failed. Used expected_lines values: {expected_lines} & ' + f'unexpected_lines values: {unexpected_lines}') + + +def remove_file(file_path: str) -> None: + """ + Remove a local file and its directory. + :param file_path: Path to the local file. + """ + if os.path.exists(file_path): + os.remove(file_path) + + file_dir = os.path.dirname(file_path) + if os.path.exists(file_dir) and len(os.listdir(file_dir)) == 0: + os.rmdir(file_dir) + + +@pytest.mark.SUITE_periodic +@pytest.mark.usefixtures('automatic_process_killer') +@pytest.mark.parametrize('project', ['AutomatedTesting']) +@pytest.mark.parametrize('level', ['AWS/Metrics']) +@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME]) +@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json']) +@pytest.mark.parametrize('profile_name', ['AWSAutomationTest']) +@pytest.mark.parametrize('region_name', ['us-west-2']) +@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests']) +@pytest.mark.parametrize('session_name', ['o3de-Automation-session']) +class TestAWSMetrics_Windows(object): + def test_AWSMetrics_RealTimeAnalytics_MetricsSentToCloudWatch(self, + level: str, + launcher: ly_test_tools.launchers.Launcher, + asset_processor: pytest.fixture, + workspace: pytest.fixture, + aws_utils: aws_utils, + aws_credentials: aws_credentials, + resource_mappings: resource_mappings, + cdk: cdk, + aws_metrics_utils: aws_metrics_utils, + ): + """ + Tests that the submitted metrics are sent to CloudWatch for real-time analytics. + """ + log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings) + + # Start the Kinesis Data Analytics application for real-time analytics. + analytics_application_name = f'{stack_name}-AnalyticsApplication' + aws_metrics_utils.start_kinesis_data_analytics_application(analytics_application_name) + + launcher.args = ['+LoadLevel', level] + launcher.args.extend(['-rhi=null']) + + with launcher.start(launch_ap=False): + start_time = datetime.utcnow() + monitor_metrics_submission(log_monitor) + # Verify that operational health metrics are delivered to CloudWatch. + aws_metrics_utils.verify_cloud_watch_delivery( + 'AWS/Lambda', + 'Invocations', + [{'Name': 'FunctionName', + 'Value': f'{stack_name}-AnalyticsProcessingLambda'}], + start_time) + logger.info('Operational health metrics sent to CloudWatch.') + + aws_metrics_utils.verify_cloud_watch_delivery( + AWS_METRICS_FEATURE_NAME, + 'TotalLogins', + [], + start_time) + logger.info('Real-time metrics sent to CloudWatch.') + + # Stop the Kinesis Data Analytics application. + aws_metrics_utils.stop_kinesis_data_analytics_application(analytics_application_name) + + def test_AWSMetrics_UnauthorizedUser_RequestRejected(self, + level: str, + launcher: ly_test_tools.launchers.Launcher, + cdk: cdk, + aws_credentials: aws_credentials, + asset_processor: pytest.fixture, + resource_mappings: resource_mappings, + workspace: pytest.fixture): + """ + Tests that unauthorized users cannot send metrics events to the AWS backed backend. + """ + log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings) + # Set invalid AWS credentials. + launcher.args = ['+LoadLevel', level, '+cl_awsAccessKey', 'AKIAIOSFODNN7EXAMPLE', + '+cl_awsSecretKey', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'] + launcher.args.extend(['-rhi=null']) + + with launcher.start(launch_ap=False): + result = log_monitor.monitor_log_for_lines( + expected_lines=['(Script) - Failed to send metrics.'], + unexpected_lines=['(Script) - Metrics is sent successfully.'], + halt_on_unexpected=True) + assert result, 'Metrics events are sent successfully by unauthorized user' + logger.info('Unauthorized user is rejected to send metrics.') + + def test_AWSMetrics_BatchAnalytics_MetricsDeliveredToS3(self, + level: str, + launcher: ly_test_tools.launchers.Launcher, + cdk: cdk, + aws_credentials: aws_credentials, + asset_processor: pytest.fixture, + resource_mappings: resource_mappings, + aws_utils: aws_utils, + aws_metrics_utils: aws_metrics_utils, + workspace: pytest.fixture): + """ + Tests that the submitted metrics are sent to the data lake for batch analytics. + """ + log_monitor, metrics_file_path, stack_name = setup(launcher, cdk, asset_processor, resource_mappings, + context_variable='batch_processing=true') + + analytics_bucket_name = aws_metrics_utils.get_analytics_bucket_name(stack_name) + + launcher.args = ['+LoadLevel', level] + launcher.args.extend(['-rhi=null']) + + with launcher.start(launch_ap=False): + start_time = datetime.utcnow() + monitor_metrics_submission(log_monitor) + # Verify that operational health metrics are delivered to CloudWatch. + aws_metrics_utils.verify_cloud_watch_delivery( + 'AWS/Lambda', + 'Invocations', + [{'Name': 'FunctionName', + 'Value': f'{stack_name}-EventsProcessingLambda'}], + start_time) + logger.info('Operational health metrics sent to CloudWatch.') + + aws_metrics_utils.verify_s3_delivery(analytics_bucket_name) + logger.info('Metrics sent to S3.') + + # Run the glue crawler to populate the AWS Glue Data Catalog with tables. + aws_metrics_utils.run_glue_crawler(f'{stack_name}-EventsCrawler') + # Run named queries on the table to verify the batch analytics. + aws_metrics_utils.run_named_queries(f'{stack_name}-AthenaWorkGroup') + logger.info('Query metrics from S3 successfully.') + + # Kinesis Data Firehose buffers incoming data before it delivers it to Amazon S3. Sleep for the + # default interval (60s) to make sure that all the metrics are sent to the bucket before cleanup. + time.sleep(60) + # Empty the S3 bucket. S3 buckets can only be deleted successfully when it doesn't contain any object. + aws_metrics_utils.empty_s3_bucket(analytics_bucket_name) + diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_utils.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_utils.py new file mode 100644 index 0000000000..686feda3d9 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_utils.py @@ -0,0 +1,252 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import logging +import pathlib +import pytest +import typing + +from datetime import datetime +from botocore.exceptions import WaiterError + +from AWS.common.aws_utils import AwsUtils +from .aws_metrics_waiters import KinesisAnalyticsApplicationUpdatedWaiter, \ + CloudWatchMetricsDeliveredWaiter, DataLakeMetricsDeliveredWaiter, GlueCrawlerReadyWaiter + +logging.getLogger('boto').setLevel(logging.CRITICAL) + +# Expected directory and file extension for the S3 objects. +EXPECTED_S3_DIRECTORY = 'firehose_events/' +EXPECTED_S3_OBJECT_EXTENSION = '.parquet' + + +class AWSMetricsUtils: + """ + Provide utils functions for the AWSMetrics gem to interact with the deployed resources. + """ + + def __init__(self, aws_utils: AwsUtils): + self._aws_util = aws_utils + + def start_kinesis_data_analytics_application(self, application_name: str) -> None: + """ + Start the Kenisis Data Analytics application for real-time analytics. + :param application_name: Name of the Kenisis Data Analytics application. + """ + input_id = self.get_kinesis_analytics_application_input_id(application_name) + assert input_id, 'invalid Kinesis Data Analytics application input.' + + client = self._aws_util.client('kinesisanalytics') + try: + client.start_application( + ApplicationName=application_name, + InputConfigurations=[ + { + 'Id': input_id, + 'InputStartingPositionConfiguration': { + 'InputStartingPosition': 'NOW' + } + }, + ] + ) + except client.exceptions.ResourceInUseException: + # The application has been started. + return + + try: + KinesisAnalyticsApplicationUpdatedWaiter(client, 'RUNNING').wait(application_name=application_name) + except WaiterError as e: + assert False, f'Failed to start the Kinesis Data Analytics application: {str(e)}.' + + def get_kinesis_analytics_application_input_id(self, application_name: str) -> str: + """ + Get the input ID for the Kenisis Data Analytics application. + :param application_name: Name of the Kenisis Data Analytics application. + :return: Input ID for the Kenisis Data Analytics application. + """ + client = self._aws_util.client('kinesisanalytics') + response = client.describe_application( + ApplicationName=application_name + ) + if not response: + return '' + input_descriptions = response.get('ApplicationDetail', {}).get('InputDescriptions', []) + if len(input_descriptions) != 1: + return '' + + return input_descriptions[0].get('InputId', '') + + def stop_kinesis_data_analytics_application(self, application_name: str) -> None: + """ + Stop the Kenisis Data Analytics application. + :param application_name: Name of the Kenisis Data Analytics application. + """ + client = self._aws_util.client('kinesisanalytics') + client.stop_application( + ApplicationName=application_name + ) + + try: + KinesisAnalyticsApplicationUpdatedWaiter(client, 'READY').wait(application_name=application_name) + except WaiterError as e: + assert False, f'Failed to stop the Kinesis Data Analytics application: {str(e)}.' + + def verify_cloud_watch_delivery(self, namespace: str, metrics_name: str, + dimensions: typing.List[dict], start_time: datetime) -> None: + """ + Verify that the expected metrics is delivered to CloudWatch. + :param namespace: Namespace of the metrics. + :param metrics_name: Name of the metrics. + :param dimensions: Dimensions of the metrics. + :param start_time: Start time for generating the metrics. + """ + client = self._aws_util.client('cloudwatch') + + try: + CloudWatchMetricsDeliveredWaiter(client).wait( + namespace=namespace, + metrics_name=metrics_name, + dimensions=dimensions, + start_time=start_time + ) + except WaiterError as e: + assert False, f'Failed to deliver metrics to CloudWatch: {str(e)}.' + + def verify_s3_delivery(self, analytics_bucket_name: str) -> None: + """ + Verify that metrics are delivered to S3 for batch analytics successfully. + :param analytics_bucket_name: Name of the deployed S3 bucket. + """ + client = self._aws_util.client('s3') + bucket_name = analytics_bucket_name + + try: + DataLakeMetricsDeliveredWaiter(client).wait(bucket_name=bucket_name, prefix=EXPECTED_S3_DIRECTORY) + except WaiterError as e: + assert False, f'Failed to find the S3 directory for storing metrics data: {str(e)}.' + + # Check whether the data is converted to the expected data format. + response = client.list_objects_v2( + Bucket=bucket_name, + Prefix=EXPECTED_S3_DIRECTORY + ) + assert response.get('KeyCount', 0) != 0, f'Failed to deliver metrics to the S3 bucket {bucket_name}.' + + s3_objects = response.get('Contents', []) + for s3_object in s3_objects: + key = s3_object.get('Key', '') + assert pathlib.Path(key).suffix == EXPECTED_S3_OBJECT_EXTENSION, \ + f'Invalid data format is found in the S3 bucket {bucket_name}' + + def run_glue_crawler(self, crawler_name: str) -> None: + """ + Run the Glue crawler and wait for it to finish. + :param crawler_name: Name of the Glue crawler + """ + client = self._aws_util.client('glue') + try: + client.start_crawler( + Name=crawler_name + ) + except client.exceptions.CrawlerRunningException: + # The crawler has already been started. + return + + try: + GlueCrawlerReadyWaiter(client).wait(crawler_name=crawler_name) + except WaiterError as e: + assert False, f'Failed to run the Glue crawler: {str(e)}.' + + def run_named_queries(self, work_group: str) -> None: + """ + Run the named queries under the specific Athena work group. + :param work_group: Name of the Athena work group. + """ + client = self._aws_util.client('athena') + # List all the named queries. + response = client.list_named_queries( + WorkGroup=work_group + ) + named_query_ids = response.get('NamedQueryIds', []) + + # Run each of the queries. + for named_query_id in named_query_ids: + get_named_query_response = client.get_named_query( + NamedQueryId=named_query_id + ) + named_query = get_named_query_response.get('NamedQuery', {}) + + start_query_execution_response = client.start_query_execution( + QueryString=named_query.get('QueryString', ''), + QueryExecutionContext={ + 'Database': named_query.get('Database', '') + }, + WorkGroup=work_group + ) + + # Wait for the query to finish. + state = 'RUNNING' + while state == 'QUEUED' or state == 'RUNNING': + get_query_execution_response = client.get_query_execution( + QueryExecutionId=start_query_execution_response.get('QueryExecutionId', '') + ) + + state = get_query_execution_response.get('QueryExecution', {}).get('Status', {}).get('State', '') + + assert state == 'SUCCEEDED', f'Failed to run the named query {named_query.get("Name", {})}' + + def empty_s3_bucket(self, bucket_name: str) -> None: + """ + Empty the S3 bucket following: + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrations3.html + + :param bucket_name: Name of the S3 bucket. + """ + + s3 = self._aws_util.resource('s3') + bucket = s3.Bucket(bucket_name) + + for key in bucket.objects.all(): + key.delete() + + def get_analytics_bucket_name(self, stack_name: str) -> str: + """ + Get the name of the deployed S3 bucket. + :param stack_name: Name of the CloudFormation stack. + :return: Name of the deployed S3 bucket. + """ + + client = self._aws_util.client('cloudformation') + + response = client.describe_stack_resources( + StackName=stack_name + ) + resources = response.get('StackResources', []) + + for resource in resources: + if resource.get('ResourceType') == 'AWS::S3::Bucket': + return resource.get('PhysicalResourceId', '') + + return '' + + +@pytest.fixture(scope='function') +def aws_metrics_utils( + request: pytest.fixture, + aws_utils: pytest.fixture): + """ + Fixture for the AWS metrics util functions. + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param aws_utils: aws_utils fixture. + """ + aws_utils_obj = AWSMetricsUtils(aws_utils) + return aws_utils_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_waiters.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_waiters.py new file mode 100644 index 0000000000..7ce5551fd4 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_waiters.py @@ -0,0 +1,142 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import botocore.client +import logging + +from datetime import timedelta +from AWS.common.custom_waiter import CustomWaiter, WaitState + +logging.getLogger('boto').setLevel(logging.CRITICAL) + + +class KinesisAnalyticsApplicationUpdatedWaiter(CustomWaiter): + """ + Subclass of the base custom waiter class. + Wait for the Kinesis analytics application being updated to a specific status. + """ + def __init__(self, client: botocore.client, status: str): + """ + Initialize the waiter. + + :param client: Boto3 client to use. + :param status: Expected status. + """ + super().__init__( + 'KinesisAnalyticsApplicationUpdated', + 'DescribeApplication', + 'ApplicationDetail.ApplicationStatus', + {status: WaitState.SUCCESS}, + client) + + def wait(self, application_name: str): + """ + Wait for the expected status. + + :param application_name: Name of the Kinesis analytics application. + """ + self._wait(ApplicationName=application_name) + + +class GlueCrawlerReadyWaiter(CustomWaiter): + """ + Subclass of the base custom waiter class. + Wait for the Glue crawler to finish its processing. + """ + def __init__(self, client: botocore.client): + """ + Initialize the waiter. + + :param client: Boto3 client to use. + """ + super().__init__( + 'GlueCrawlerReady', + 'GetCrawler', + 'Crawler.State', + {'READY': WaitState.SUCCESS}, + client) + + def wait(self, crawler_name): + """ + Wait for the expected status. + + :param crawler_name: Name of the Glue crawler. + """ + self._wait(Name=crawler_name) + + +class DataLakeMetricsDeliveredWaiter(CustomWaiter): + """ + Subclass of the base custom waiter class. + Wait for the expected directory being created in the S3 bucket. + """ + def __init__(self, client: botocore.client): + """ + Initialize the waiter. + + :param client: Boto3 client to use. + """ + super().__init__( + 'DataLakeMetricsDelivered', + 'ListObjectsV2', + 'KeyCount > `0`', + {True: WaitState.SUCCESS}, + client) + + def wait(self, bucket_name, prefix): + """ + Wait for the expected directory being created. + + :param bucket_name: Name of the S3 bucket. + :param prefix: Name of the expected directory prefix. + """ + self._wait(Bucket=bucket_name, Prefix=prefix) + + +class CloudWatchMetricsDeliveredWaiter(CustomWaiter): + """ + Subclass of the base custom waiter class. + Wait for the expected metrics being delivered to CloudWatch. + """ + def __init__(self, client: botocore.client): + """ + Initialize the waiter. + + :param client: Boto3 client to use. + """ + super().__init__( + 'CloudWatchMetricsDelivered', + 'GetMetricStatistics', + 'length(Datapoints) > `0`', + {True: WaitState.SUCCESS}, + client) + + def wait(self, namespace, metrics_name, dimensions, start_time): + """ + Wait for the expected metrics being delivered. + + :param namespace: Namespace of the metrics. + :param metrics_name: Name of the metrics. + :param dimensions: Dimensions of the metrics. + :param start_time: Start time for generating the metrics. + """ + self._wait( + Namespace=namespace, + MetricName=metrics_name, + Dimensions=dimensions, + StartTime=start_time, + EndTime=start_time + timedelta(0, self.timeout), + Period=60, + Statistics=[ + 'SampleCount' + ], + Unit='Count' + ) diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py index 455b3f94cb..5dbde29b9d 100644 --- a/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py +++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/cdk/cdk.py @@ -16,12 +16,15 @@ import boto3 import ly_test_tools.environment.process_utils as process_utils from typing import List +BOOTSTRAP_STACK_NAME = 'CDKToolkit' +BOOTSTRAP_STAGING_BUCKET_LOGIC_ID = 'StagingBucket' class Cdk: """ Cdk class that provides methods to run cdk application commands. Expects system to have NodeJS, AWS CLI and CDK installed globally and have their paths setup as env variables. """ + def __init__(self, cdk_path: str, project: str, account_id: str, workspace: pytest.fixture, session: boto3.session.Session): """ @@ -49,12 +52,24 @@ class Cdk: env=self._cdk_env, shell=True) + def bootstrap(self) -> None: + """ + Deploy the bootstrap stack. + """ + bootstrap_cmd = ['cdk', 'bootstrap', + f'aws://{self._cdk_env["O3DE_AWS_DEPLOY_ACCOUNT"]}/{self._cdk_env["O3DE_AWS_DEPLOY_REGION"]}'] + + process_utils.check_call( + bootstrap_cmd, + cwd=self._cdk_path, + env=self._cdk_env, + shell=True) + def list(self) -> List[str]: """ lists cdk stack names :return List of cdk stack names """ - if not self._cdk_path: return [] @@ -123,6 +138,38 @@ class Cdk: self._stacks = [] self._cdk_path = '' + @staticmethod + def remove_bootstrap_stack(aws_utils: pytest.fixture) -> None: + """ + Remove the CDK bootstrap stack. + :param aws_utils: aws_utils fixture. + """ + # Check if the bootstrap stack exists. + response = aws_utils.client('cloudformation').describe_stacks( + StackName=BOOTSTRAP_STACK_NAME + ) + stacks = response.get('Stacks', []) + if not stacks: + return + + # Clear the bootstrap staging bucket before deleting the bootstrap stack. + response = aws_utils.client('cloudformation').describe_stack_resource( + StackName=BOOTSTRAP_STACK_NAME, + LogicalResourceId=BOOTSTRAP_STAGING_BUCKET_LOGIC_ID + ) + + staging_bucket_name = response.get('StackResourceDetail', {}).get('PhysicalResourceId', '') + if staging_bucket_name: + s3 = aws_utils.resource('s3') + bucket = s3.Bucket(staging_bucket_name) + for key in bucket.objects.all(): + key.delete() + + # Delete the bootstrap stack. + aws_utils.client('cloudformation').delete_stack( + StackName=BOOTSTRAP_STACK_NAME + ) + @pytest.fixture(scope='function') def cdk( @@ -131,6 +178,7 @@ def cdk( feature_name: str, workspace: pytest.fixture, aws_utils: pytest.fixture, + bootstrap_required: bool = True, destroy_stacks_on_teardown: bool = True) -> Cdk: """ Fixture for setting up a Cdk @@ -140,6 +188,8 @@ def cdk( :param feature_name: Feature gem name to expect cdk folder in. :param workspace: ly_test_tools workspace fixture. :param aws_utils: aws_utils fixture. + :param bootstrap_required: Whether the bootstrap stack needs to be created to + provision resources the AWS CDK needs to perform the deployment. :param destroy_stacks_on_teardown: option to control calling destroy ot the end of test. :return Cdk class object. """ @@ -147,9 +197,14 @@ def cdk( cdk_path = f'{workspace.paths.engine_root()}/Gems/{feature_name}/cdk' cdk_obj = Cdk(cdk_path, project, aws_utils.assume_account_id(), workspace, aws_utils.assume_session()) + if bootstrap_required: + cdk_obj.bootstrap() + def teardown(): if destroy_stacks_on_teardown: cdk_obj.destroy() + cdk_obj.remove_bootstrap_stack(aws_utils) + request.addfinalizer(teardown) return cdk_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/common/aws_credentials.py b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_credentials.py new file mode 100644 index 0000000000..fbce772d40 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_credentials.py @@ -0,0 +1,134 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import boto3 +import configparser +import logging +import os +import pytest +import typing + +logger = logging.getLogger(__name__) +logging.getLogger('boto').setLevel(logging.CRITICAL) + + +class AwsCredentials: + def __init__(self, profile_name: str): + self._profile_name = profile_name + + self._credentials_path = os.environ.get('AWS_SHARED_CREDENTIALS_FILE') + if not self._credentials_path: + # Home directory location varies based on the operating system, but is referred to using the environment + # variables %UserProfile% in Windows and $HOME or ~ (tilde) in Unix-based systems. + self._credentials_path = os.path.join(os.environ.get('UserProfile', os.path.expanduser('~')), + '.aws', 'credentials') + self._credentials_file_exists = os.path.exists(self._credentials_path) + + self._credentials = configparser.ConfigParser() + self._credentials.read(self._credentials_path) + + def get_aws_credentials(self) -> typing.Tuple[str, str, str]: + """ + Get aws credentials stored in the specific named profile. + + :return AWS credentials. + """ + access_key_id = self._get_aws_credential_attribute_value('aws_access_key_id') + secret_access_key = self._get_aws_credential_attribute_value('aws_secret_access_key') + session_token = self._get_aws_credential_attribute_value('aws_session_token') + + return access_key_id, secret_access_key, session_token + + def set_aws_credentials_by_session(self, session: boto3.Session) -> None: + """ + Set AWS credentials stored in the specific named profile using an assumed role session. + + :param session: assumed role session. + """ + credentials = session.get_credentials().get_frozen_credentials() + self.set_aws_credentials(credentials.access_key, credentials.secret_key, credentials.token) + + def set_aws_credentials(self, aws_access_key_id: str, aws_secret_access_key: str, + aws_session_token: str) -> None: + """ + Set AWS credentials stored in the specific named profile. + + :param aws_access_key_id: AWS access key id. + :param aws_secret_access_key: AWS secrete access key. + :param aws_session_token: AWS assumed role session. + """ + self._set_aws_credential_attribute_value('aws_access_key_id', aws_access_key_id) + self._set_aws_credential_attribute_value('aws_secret_access_key', aws_secret_access_key) + self._set_aws_credential_attribute_value('aws_session_token', aws_session_token) + + if (len(self._credentials.sections()) == 0) and (not self._credentials_file_exists): + os.remove(self._credentials_path) + return + + with open(self._credentials_path, 'w+') as credential_file: + self._credentials.write(credential_file) + + def _get_aws_credential_attribute_value(self, attribute_name: str) -> str: + """ + Get the value of an AWS credential attribute stored in the specific named profile. + + :param attribute_name: Name of the AWS credential attribute. + :return Value of the AWS credential attribute. + """ + try: + value = self._credentials.get(self._profile_name, attribute_name) + except configparser.NoSectionError: + # Named profile or key doesn't exist + value = None + except configparser.NoOptionError: + # Named profile doesn't have the specified attribute + value = None + + return value + + def _set_aws_credential_attribute_value(self, attribute_name: str, attribute_value: str) -> None: + """ + Set the value of an AWS credential attribute stored in the specific named profile. + + :param attribute_name: Name of the AWS credential attribute. + :param attribute_value: Value of the AWS credential attribute. + """ + if self._profile_name not in self._credentials: + self._credentials[self._profile_name] = {} + + if attribute_value is None: + self._credentials.remove_option(self._profile_name, attribute_name) + # Remove the named profile if it doesn't have any AWS credential attribute. + if len(self._credentials[self._profile_name]) == 0: + self._credentials.remove_section(self._profile_name) + else: + self._credentials[self._profile_name][attribute_name] = attribute_value + + +@pytest.fixture(scope='function') +def aws_credentials(request: pytest.fixture, aws_utils: pytest.fixture, profile_name: str): + """ + Fixture for setting up temporary AWS credentials from assume role. + + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param aws_utils: aws_utils fixture. + :param profile_name: Named AWS profile to store temporary credentials. + """ + aws_credentials_obj = AwsCredentials(profile_name) + original_access_key, original_secret_access_key, original_token = aws_credentials_obj.get_aws_credentials() + aws_credentials_obj.set_aws_credentials_by_session(aws_utils.assume_session()) + + def teardown(): + # Reset to the named profile using the original AWS credentials + aws_credentials_obj.set_aws_credentials(original_access_key, original_secret_access_key, original_token) + request.addfinalizer(teardown) + + return aws_credentials_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py index 7a15ba0abe..ff33f58d1d 100644 --- a/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py +++ b/AutomatedTesting/Gem/PythonTests/AWS/common/aws_utils.py @@ -1,82 +1,90 @@ -""" -All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or -its licensors. -For complete copyright and license terms please see the LICENSE at the root of this -distribution (the "License"). All use of this software is governed by the License, -or, if provided, by the license below or the license accompanying this file. Do not -remove or modify any license notices. This file is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -""" -import boto3 -import pytest -import logging - -logger = logging.getLogger(__name__) - - -class AwsUtils: - - def __init__(self, arn: str, session_name: str, region_name: str): - local_session = boto3.Session(profile_name='default') - local_sts_client = local_session.client('sts') - self._local_account_id = local_sts_client.get_caller_identity()["Account"] - logger.info(f'Local Account Id: {self._local_account_id}') - - response = local_sts_client.assume_role(RoleArn=arn, RoleSessionName=session_name) - - self._assume_session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'], - aws_secret_access_key=response['Credentials']['SecretAccessKey'], - aws_session_token=response['Credentials']['SessionToken'], - region_name=region_name) - - assume_sts_client = self._assume_session.client('sts') - assume_account_id = assume_sts_client.get_caller_identity()["Account"] - logger.info(f'Assume Account Id: {assume_account_id}') - self._assume_account_id = assume_account_id - - def client(self, service: str): - """ - Get the client for a specific AWS service from configured session - :return: Client for the AWS service. - """ - return self._assume_session.client(service) - - def assume_session(self): - return self._assume_session - - def local_account_id(self): - return self._local_account_id - - def assume_account_id(self): - return self._assume_account_id - - def destroy(self) -> None: - """ - clears stored session - """ - self._assume_session = None - - -@pytest.fixture(scope='function') -def aws_utils( - request: pytest.fixture, - assume_role_arn: str, - session_name: str, - region_name: str): - """ - Fixture for setting up a Cdk - :param request: _pytest.fixtures.SubRequest class that handles getting - a pytest fixture from a pytest function/fixture. - :param assume_role_arn: Role used to fetch temporary aws credentials, configure service clients with obtained credentials. - :param session_name: Session name to set. - :param region_name: AWS account region to set for session. - :return AWSUtils class object. - """ - aws_utils_obj = AwsUtils(assume_role_arn, session_name, region_name) - - def teardown(): - aws_utils_obj.destroy() - - request.addfinalizer(teardown) - - return aws_utils_obj +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" +import boto3 +import pytest +import logging + +logger = logging.getLogger(__name__) +logging.getLogger('boto').setLevel(logging.CRITICAL) + + +class AwsUtils: + + def __init__(self, arn: str, session_name: str, region_name: str): + local_session = boto3.Session(profile_name='default') + local_sts_client = local_session.client('sts') + self._local_account_id = local_sts_client.get_caller_identity()["Account"] + logger.info(f'Local Account Id: {self._local_account_id}') + + response = local_sts_client.assume_role(RoleArn=arn, RoleSessionName=session_name) + + self._assume_session = boto3.Session(aws_access_key_id=response['Credentials']['AccessKeyId'], + aws_secret_access_key=response['Credentials']['SecretAccessKey'], + aws_session_token=response['Credentials']['SessionToken'], + region_name=region_name) + + assume_sts_client = self._assume_session.client('sts') + assume_account_id = assume_sts_client.get_caller_identity()["Account"] + logger.info(f'Assume Account Id: {assume_account_id}') + self._assume_account_id = assume_account_id + + def client(self, service: str): + """ + Get the client for a specific AWS service from configured session + :return: Client for the AWS service. + """ + return self._assume_session.client(service) + + def resource(self, service: str): + """ + Get the resource for a specific AWS service from configured session + :return: Client for the AWS service. + """ + return self._assume_session.resource(service) + + def assume_session(self): + return self._assume_session + + def local_account_id(self): + return self._local_account_id + + def assume_account_id(self): + return self._assume_account_id + + def destroy(self) -> None: + """ + clears stored session + """ + self._assume_session = None + + +@pytest.fixture(scope='function') +def aws_utils( + request: pytest.fixture, + assume_role_arn: str, + session_name: str, + region_name: str): + """ + Fixture for AWS util functions + :param request: _pytest.fixtures.SubRequest class that handles getting + a pytest fixture from a pytest function/fixture. + :param assume_role_arn: Role used to fetch temporary aws credentials, configure service clients with obtained credentials. + :param session_name: Session name to set. + :param region_name: AWS account region to set for session. + :return AWSUtils class object. + """ + aws_utils_obj = AwsUtils(assume_role_arn, session_name, region_name) + + def teardown(): + aws_utils_obj.destroy() + + request.addfinalizer(teardown) + + return aws_utils_obj diff --git a/AutomatedTesting/Gem/PythonTests/AWS/common/custom_waiter.py b/AutomatedTesting/Gem/PythonTests/AWS/common/custom_waiter.py new file mode 100644 index 0000000000..7c0a65e8a3 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/AWS/common/custom_waiter.py @@ -0,0 +1,91 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from enum import Enum +import botocore.client +import botocore.waiter +import logging + +logging.getLogger('boto').setLevel(logging.CRITICAL) + + +class WaitState(Enum): + SUCCESS = 'success' + FAILURE = 'failure' + + +class CustomWaiter: + """ + Base class for a custom waiter. + + Modified from: + https://docs.aws.amazon.com/code-samples/latest/catalog/python-demo_tools-custom_waiter.py.html + """ + def __init__( + self, name: str, operation: str, argument: str, + acceptors: dict, client: botocore.client, delay: int = 30, max_tries: int = 10, + matcher='path'): + """ + Subclasses should pass specific operations, arguments, and acceptors to + their superclass. + + :param name: The name of the waiter. This can be any descriptive string. + :param operation: The operation to wait for. This must match the casing of + the underlying operation model, which is typically in + CamelCase. + :param argument: The dict keys used to access the result of the operation, in + dot notation. For example, 'Job.Status' will access + result['Job']['Status']. + :param acceptors: The list of acceptors that indicate the wait is over. These + can indicate either success or failure. The acceptor values + are compared to the result of the operation after the + argument keys are applied. + :param client: The Boto3 client. + :param delay: The number of seconds to wait between each call to the operation. Default to 30 seconds. + :param max_tries: The maximum number of tries before exiting. Default to 10. + :param matcher: The kind of matcher to use. Default to 'path'. + """ + self.name = name + self.operation = operation + self.argument = argument + self.client = client + self.waiter_model = botocore.waiter.WaiterModel({ + 'version': 2, + 'waiters': { + name: { + "delay": delay, + "operation": operation, + "maxAttempts": max_tries, + "acceptors": [{ + "state": state.value, + "matcher": matcher, + "argument": argument, + "expected": expected + } for expected, state in acceptors.items()] + }}}) + self.waiter = botocore.waiter.create_waiter_with_client( + self.name, self.waiter_model, self.client) + + self._timeout = delay * max_tries + + def _wait(self, **kwargs): + """ + Starts the botocore wait loop. + + :param kwargs: Keyword arguments that are passed to the operation being polled. + """ + self.waiter.wait(**kwargs) + + @property + def timeout(self): + return self._timeout + + diff --git a/AutomatedTesting/Registry/awscoreconfiguration.setreg b/AutomatedTesting/Registry/awscoreconfiguration.setreg index ca110eb103..b7c60b0fb9 100644 --- a/AutomatedTesting/Registry/awscoreconfiguration.setreg +++ b/AutomatedTesting/Registry/awscoreconfiguration.setreg @@ -3,7 +3,7 @@ { "AWSCore": { - "ProfileName": "default", + "ProfileName": "AWSAutomationTest", "ResourceMappingConfigFileName": "aws_resource_mappings.json" } } diff --git a/Gems/AWSMetrics/cdk/aws_metrics/batch_processing.py b/Gems/AWSMetrics/cdk/aws_metrics/batch_processing.py index fe85615d8d..22f6ad6903 100755 --- a/Gems/AWSMetrics/cdk/aws_metrics/batch_processing.py +++ b/Gems/AWSMetrics/cdk/aws_metrics/batch_processing.py @@ -111,8 +111,7 @@ class BatchProcessing: self._events_firehose_delivery_stream = kinesisfirehose.CfnDeliveryStream( self._stack, - id='EventsFirehoseDeliveryStream', - delivery_stream_name=f'{self._stack.stack_name}-EventsFirehoseDeliveryStream', + id=f'{self._stack.stack_name}-EventsFirehoseDeliveryStream', delivery_stream_type='KinesisStreamAsSource', kinesis_stream_source_configuration=kinesisfirehose.CfnDeliveryStream.KinesisStreamSourceConfigurationProperty( kinesis_stream_arn=self._input_stream_arn, @@ -327,7 +326,7 @@ class BatchProcessing: @property def delivery_stream_name(self) -> kinesisfirehose.CfnDeliveryStream.delivery_stream_name: - return self._events_firehose_delivery_stream.delivery_stream_name + return self._events_firehose_delivery_stream.ref @property def delivery_stream_role_arn(self) -> iam.Role.role_arn: diff --git a/Gems/AWSMetrics/cdk/aws_metrics/data_ingestion.py b/Gems/AWSMetrics/cdk/aws_metrics/data_ingestion.py index a3ab99fccf..fbd332a577 100755 --- a/Gems/AWSMetrics/cdk/aws_metrics/data_ingestion.py +++ b/Gems/AWSMetrics/cdk/aws_metrics/data_ingestion.py @@ -73,14 +73,14 @@ class DataIngestion: api_id_output = core.CfnOutput( self._stack, - id='RestApiId', + id='RESTApiId', description='Service API Id for the analytics pipeline', export_name=f"{application_name}:RestApiId", value=self._rest_api.rest_api_id) stage_output = core.CfnOutput( self._stack, - id='DeploymentStage', + id='RESTApiStage', description='Stage for the REST API deployment', export_name=f"{application_name}:DeploymentStage", value=self._rest_api.deployment_stage.stage_name) From 9e3b7b45d95e6a8fe4af6340569f1d7f356674c4 Mon Sep 17 00:00:00 2001 From: hershey5045 <43485729+hershey5045@users.noreply.github.com> Date: Tue, 1 Jun 2021 14:21:02 -0700 Subject: [PATCH 43/53] Fix window handle retrieval in frame capture system. (#1073) Capturing screenshots via hydra should work now. --- .../Common/Code/Source/FrameCaptureSystemComponent.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp index f8c1258fc0..0d9e46a418 100644 --- a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -289,11 +290,7 @@ namespace AZ bool FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath) { - AzFramework::NativeWindowHandle windowHandle = nullptr; - AzFramework::WindowSystemRequestBus::BroadcastResult( - windowHandle, - &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle); - + AzFramework::NativeWindowHandle windowHandle = AZ::RPI::ViewportContextRequests::Get()->GetDefaultViewportContext()->GetWindowHandle(); if (windowHandle) { return CaptureScreenshotForWindow(filePath, windowHandle); From afe20906db9c9f02003eed21a460627b51523ea5 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:30:14 -0700 Subject: [PATCH 44/53] Add Project Manager File menu options to Editor --- .../ProjectManager/ProjectManager.cpp | 6 ++-- .../ProjectManager/ProjectManager.h | 15 ++++++-- .../Editor/Core/LevelEditorMenuHandler.cpp | 11 +++--- Code/Sandbox/Editor/CryEdit.cpp | 34 +++++++++++++++++++ Code/Sandbox/Editor/CryEdit.h | 4 +++ Code/Sandbox/Editor/LyViewPaneNames.h | 2 +- Code/Sandbox/Editor/MainWindow.cpp | 3 ++ Code/Sandbox/Editor/Resource.h | 3 ++ .../Source/ProjectManagerWindow.cpp | 18 ++++++++-- .../Source/ProjectManagerWindow.h | 4 ++- .../ProjectManager/Source/ProjectUtils.cpp | 11 ++++++ .../ProjectManager/Source/ProjectUtils.h | 2 ++ Code/Tools/ProjectManager/Source/ScreenDefs.h | 23 ++++++++++++- Code/Tools/ProjectManager/Source/main.cpp | 28 ++++++++++++++- 14 files changed, 147 insertions(+), 17 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.cpp b/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.cpp index 2742b90f4c..22598595fc 100644 --- a/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.cpp @@ -78,7 +78,7 @@ namespace AzFramework::ProjectManager projectJsonPath.c_str()); } - if (LaunchProjectManager(engineRootPath)) + if (LaunchProjectManager()) { AZ_TracePrintf("ProjectManager", "Project Manager launched successfully, requesting exit."); return ProjectPathCheckResult::ProjectManagerLaunched; @@ -87,7 +87,7 @@ namespace AzFramework::ProjectManager return ProjectPathCheckResult::ProjectManagerLaunchFailed; } - bool LaunchProjectManager([[maybe_unused]] const AZ::IO::FixedMaxPath& engineRootPath) + bool LaunchProjectManager(const AZStd::string& commandLineArgs) { bool launchSuccess = false; #if (AZ_TRAIT_AZFRAMEWORK_USE_PROJECT_MANAGER) @@ -109,7 +109,7 @@ namespace AzFramework::ProjectManager } AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = executablePath.String(); + processLaunchInfo.m_commandlineParameters = executablePath.String() + commandLineArgs; launchSuccess = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo); } if (ownsSystemAllocator) diff --git a/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.h b/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.h index cc79bd4184..d0ef7172b0 100644 --- a/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.h +++ b/Code/Framework/AzFramework/AzFramework/ProjectManager/ProjectManager.h @@ -12,6 +12,7 @@ #pragma once #include +#include namespace AzFramework::ProjectManager { @@ -21,8 +22,16 @@ namespace AzFramework::ProjectManager ProjectManagerLaunched = 0, ProjectPathFound = 1 }; - // Check for a project name, if not found, attempts to launch project manager and returns false + + //! Check for a project name, if not found, attempts to launch project manager and returns false + //! @param argc the number of arguments in argv + //! @param argv arguments provided to this executable + //! @return a ProjectPathCheckResult ProjectPathCheckResult CheckProjectPathProvided(const int argc, char* argv[]); - // Attempt to Launch the project manager. Requires locating the engine root, project manager script, and python. - bool LaunchProjectManager(const AZ::IO::FixedMaxPath& engineRootPath); + + //! Attempt to Launch the project manager, assuming the o3de executable exists in same folder as + //! current executable. Requires the o3de cli and python. + //! @param commandLineArgs additional command line arguments to provide to the project manager + //! @return true on success, false if failed to find or launch the executable + bool LaunchProjectManager(const AZStd::string& commandLineArgs = ""); } // AzFramework::ProjectManager diff --git a/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp b/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp index 0ce3fa55f5..8f6e927a84 100644 --- a/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp +++ b/Code/Sandbox/Editor/Core/LevelEditorMenuHandler.cpp @@ -421,17 +421,18 @@ QMenu* LevelEditorMenuHandler::CreateFileMenu() fileMenu.AddSeparator(); // Project Settings - auto projectSettingMenu = fileMenu.AddMenu(tr("Project Settings")); + fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_SETTINGS); - // Project Settings Tool + // Platform Settings - Project Settings Tool // Shortcut must be set while adding the action otherwise it doesn't work - projectSettingMenu.Get()->addAction( + fileMenu.Get()->addAction( tr(LyViewPane::ProjectSettingsTool), []() { QtViewPaneManager::instance()->OpenPane(LyViewPane::ProjectSettingsTool); }, tr("Ctrl+Shift+P")); - projectSettingMenu.AddSeparator(); - + fileMenu.AddSeparator(); + fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_NEW); + fileMenu.AddAction(ID_FILE_PROJECT_MANAGER_OPEN); fileMenu.AddSeparator(); // NEWMENUS: NEEDS IMPLEMENTATION diff --git a/Code/Sandbox/Editor/CryEdit.cpp b/Code/Sandbox/Editor/CryEdit.cpp index a0ff5d7eff..c60f32560b 100644 --- a/Code/Sandbox/Editor/CryEdit.cpp +++ b/Code/Sandbox/Editor/CryEdit.cpp @@ -58,6 +58,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include // AzToolsFramework #include @@ -477,6 +478,11 @@ void CCryEditApp::RegisterActionHandlers() ON_COMMAND(ID_FILE_SAVE_LEVEL, OnFileSave) ON_COMMAND(ID_FILE_EXPORTOCCLUSIONMESH, OnFileExportOcclusionMesh) + + // Project Manager + ON_COMMAND(ID_FILE_PROJECT_MANAGER_SETTINGS, OnOpenProjectManagerSettings) + ON_COMMAND(ID_FILE_PROJECT_MANAGER_NEW, OnOpenProjectManagerNew) + ON_COMMAND(ID_FILE_PROJECT_MANAGER_OPEN, OnOpenProjectManager) } CCryEditApp* CCryEditApp::s_currentInstance = nullptr; @@ -2854,6 +2860,34 @@ void CCryEditApp::OnPreferences() */ } +void CCryEditApp::OnOpenProjectManagerSettings() +{ + OpenProjectManager("UpdateProject"); +} + +void CCryEditApp::OnOpenProjectManagerNew() +{ + OpenProjectManager("CreateProject"); +} + +void CCryEditApp::OnOpenProjectManager() +{ + OpenProjectManager("Projects"); +} + +void CCryEditApp::OpenProjectManager(const AZStd::string& screen) +{ + // provide the current project path for in case we want to update the project + AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath(); + const AZStd::string commandLineOptions = AZStd::string::format(" --screen %s --project_path %s", screen.c_str(), projectPath.c_str()); + bool launchSuccess = AzFramework::ProjectManager::LaunchProjectManager(commandLineOptions); + if (!launchSuccess) + { + QMessageBox::critical(AzToolsFramework::GetActiveWindow(), QObject::tr("Failed to launch O3DE Project Manager"), QObject::tr("Failed to find or start the O3dE Project Manager")); + } +} + + ////////////////////////////////////////////////////////////////////////// void CCryEditApp::OnUndo() { diff --git a/Code/Sandbox/Editor/CryEdit.h b/Code/Sandbox/Editor/CryEdit.h index d4c1304b6a..dc4f015faf 100644 --- a/Code/Sandbox/Editor/CryEdit.h +++ b/Code/Sandbox/Editor/CryEdit.h @@ -229,6 +229,9 @@ public: void OnFileResaveSlices(); void OnFileEditEditorini(); void OnPreferences(); + void OnOpenProjectManagerSettings(); + void OnOpenProjectManagerNew(); + void OnOpenProjectManager(); void OnRedo(); void OnUpdateRedo(QAction* action); void OnUpdateUndo(QAction* action); @@ -366,6 +369,7 @@ private: AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING friend struct PythonTestOutputHandler; + void OpenProjectManager(const AZStd::string& screen); void OnWireframe(); void OnUpdateWireframe(QAction* action); void OnViewConfigureLayout(); diff --git a/Code/Sandbox/Editor/LyViewPaneNames.h b/Code/Sandbox/Editor/LyViewPaneNames.h index e95191ce06..b94cda3c52 100644 --- a/Code/Sandbox/Editor/LyViewPaneNames.h +++ b/Code/Sandbox/Editor/LyViewPaneNames.h @@ -30,7 +30,7 @@ namespace LyViewPane static const char* const EntityInspector = "Entity Inspector"; static const char* const EntityInspectorPinned = "Pinned Entity Inspector"; static const char* const LevelInspector = "Level Inspector"; - static const char* const ProjectSettingsTool = "Project Settings Tool"; + static const char* const ProjectSettingsTool = "Edit Platform Settings..."; static const char* const ErrorReport = "Error Report"; static const char* const Console = "Console"; static const char* const ConsoleMenuName = "&Console"; diff --git a/Code/Sandbox/Editor/MainWindow.cpp b/Code/Sandbox/Editor/MainWindow.cpp index 8086293207..9e983c3593 100644 --- a/Code/Sandbox/Editor/MainWindow.cpp +++ b/Code/Sandbox/Editor/MainWindow.cpp @@ -748,6 +748,9 @@ void MainWindow::InitActions() am->AddAction(ID_FILE_EXPORTOCCLUSIONMESH, tr("Export Occlusion Mesh")); am->AddAction(ID_FILE_EDITLOGFILE, tr("Show Log File")); am->AddAction(ID_FILE_RESAVESLICES, tr("Resave All Slices")); + am->AddAction(ID_FILE_PROJECT_MANAGER_SETTINGS, tr("Edit Project Settings...")); + am->AddAction(ID_FILE_PROJECT_MANAGER_NEW, tr("New Project...")); + am->AddAction(ID_FILE_PROJECT_MANAGER_OPEN, tr("Open Project...")); am->AddAction(ID_GAME_PC_ENABLEVERYHIGHSPEC, tr("Very High")).SetCheckable(true) .RegisterUpdateCallback(cryEdit, &CCryEditApp::OnUpdateGameSpec); am->AddAction(ID_GAME_PC_ENABLEHIGHSPEC, tr("High")).SetCheckable(true) diff --git a/Code/Sandbox/Editor/Resource.h b/Code/Sandbox/Editor/Resource.h index 31fc9909f1..9c50045367 100644 --- a/Code/Sandbox/Editor/Resource.h +++ b/Code/Sandbox/Editor/Resource.h @@ -313,6 +313,9 @@ #define ID_CREATE_LEVEL_FG_MODULE_FROM_SELECTION 35077 #define ID_GRAPHVIEW_ADD_BLACK_BOX 35078 #define ID_GRAPHVIEW_UNGROUP 35079 +#define ID_FILE_PROJECT_MANAGER_NEW 35080 +#define ID_FILE_PROJECT_MANAGER_OPEN 35081 +#define ID_FILE_PROJECT_MANAGER_SETTINGS 35082 #define ID_TV_TRACKS_TOOLBAR_BASE 35083 // range between ID_TV_TRACKS_TOOLBAR_BASE to ID_TV_TRACKS_TOOLBAR_LAST reserved #define ID_TV_TRACKS_TOOLBAR_LAST 35183 // for up to 100 "Add Tracks..." dynamically added Track View Track buttons #define ID_OPEN_TERRAIN_EDITOR 36007 diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index 76bcc2eb99..cb1398cc61 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -14,13 +14,16 @@ #include #include +#include #include +#include +#include #include namespace O3DE::ProjectManager { - ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath) + ProjectManagerWindow::ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, ProjectManagerScreen startScreen) : QMainWindow(parent) { m_pythonBindings = AZStd::make_unique(engineRootPath); @@ -50,7 +53,18 @@ namespace O3DE::ProjectManager // set stylesheet after creating the screens or their styles won't get updated AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss")); - screensCtrl->ForceChangeToScreen(ProjectManagerScreen::Projects, false); + // always push the projects screen first so we have something to come back to + if (startScreen != ProjectManagerScreen::Projects) + { + screensCtrl->ForceChangeToScreen(ProjectManagerScreen::Projects); + } + screensCtrl->ForceChangeToScreen(startScreen); + + if (!projectPath.empty()) + { + const QString path = QString::fromUtf8(projectPath.Native().data(), aznumeric_cast(projectPath.Native().size())); + emit screensCtrl->NotifyCurrentProject(path); + } } ProjectManagerWindow::~ProjectManagerWindow() diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h index 74db3467c5..758af8fc00 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.h @@ -14,6 +14,7 @@ #if !defined(Q_MOC_RUN) #include #include +#include #endif namespace O3DE::ProjectManager @@ -24,7 +25,8 @@ namespace O3DE::ProjectManager Q_OBJECT public: - explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath); + explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& engineRootPath, const AZ::IO::PathView& projectPath, + ProjectManagerScreen startScreen = ProjectManagerScreen::Projects); ~ProjectManagerWindow(); private: diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.cpp b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp index 526e745d82..58e4c5c60f 100644 --- a/Code/Tools/ProjectManager/Source/ProjectUtils.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp @@ -192,5 +192,16 @@ namespace O3DE::ProjectManager return true; } + ProjectManagerScreen GetProjectManagerScreen(const QString& screen) + { + auto iter = s_ProjectManagerStringNames.find(screen); + if (iter != s_ProjectManagerStringNames.end()) + { + return iter.value(); + } + + return ProjectManagerScreen::Invalid; + } + } // namespace ProjectUtils } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.h b/Code/Tools/ProjectManager/Source/ProjectUtils.h index 5982bff634..d556d682f2 100644 --- a/Code/Tools/ProjectManager/Source/ProjectUtils.h +++ b/Code/Tools/ProjectManager/Source/ProjectUtils.h @@ -11,6 +11,7 @@ */ #pragma once +#include #include namespace O3DE::ProjectManager @@ -24,5 +25,6 @@ namespace O3DE::ProjectManager bool CopyProject(const QString& origPath, const QString& newPath); bool DeleteProjectFiles(const QString& path, bool force = false); bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr); + ProjectManagerScreen GetProjectManagerScreen(const QString& screen); } // namespace ProjectUtils } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ScreenDefs.h b/Code/Tools/ProjectManager/Source/ScreenDefs.h index 46d243f677..43ed303461 100644 --- a/Code/Tools/ProjectManager/Source/ScreenDefs.h +++ b/Code/Tools/ProjectManager/Source/ScreenDefs.h @@ -11,9 +11,13 @@ */ #pragma once +#include +#include +#include + namespace O3DE::ProjectManager { - enum ProjectManagerScreen + enum class ProjectManagerScreen { Invalid = -1, Empty, @@ -25,4 +29,21 @@ namespace O3DE::ProjectManager ProjectSettings, EngineSettings }; + + static QHash s_ProjectManagerStringNames = { + { "Empty", ProjectManagerScreen::Empty}, + { "CreateProject", ProjectManagerScreen::CreateProject}, + { "NewProjectSettings", ProjectManagerScreen::NewProjectSettings}, + { "GemCatalog", ProjectManagerScreen::GemCatalog}, + { "Projects", ProjectManagerScreen::Projects}, + { "UpdateProject", ProjectManagerScreen::UpdateProject}, + { "ProjectSettings", ProjectManagerScreen::ProjectSettings}, + { "EngineSettings", ProjectManagerScreen::EngineSettings} + }; + + // need to define qHash for ProjectManagerScreen when using scoped enums + inline uint qHash(ProjectManagerScreen key, uint seed) + { + return ::qHash(static_cast(key), seed); + } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/main.cpp b/Code/Tools/ProjectManager/Source/main.cpp index cbeacbaf65..c597b8a729 100644 --- a/Code/Tools/ProjectManager/Source/main.cpp +++ b/Code/Tools/ProjectManager/Source/main.cpp @@ -15,13 +15,17 @@ #include #include #include +#include #include +#include #include #include #include +using namespace O3DE::ProjectManager; + int main(int argc, char* argv[]) { QApplication::setOrganizationName("O3DE"); @@ -51,7 +55,29 @@ int main(int argc, char* argv[]) AzQtComponents::StyleManager styleManager(&app); styleManager.initialize(&app, engineRootPath); - O3DE::ProjectManager::ProjectManagerWindow window(nullptr, engineRootPath); + // Get the initial start screen if one is provided via command line + constexpr char optionPrefix[] = "--"; + AZ::CommandLine commandLine(optionPrefix); + commandLine.Parse(argc, argv); + + ProjectManagerScreen startScreen = ProjectManagerScreen::Projects; + if(commandLine.HasSwitch("screen")) + { + QString screenOption = commandLine.GetSwitchValue("screen", 0).c_str(); + ProjectManagerScreen screen = ProjectUtils::GetProjectManagerScreen(screenOption); + if (screen != ProjectManagerScreen::Invalid) + { + startScreen = screen; + } + } + + AZ::IO::FixedMaxPath projectPath; + if (commandLine.HasSwitch("project-path")) + { + projectPath = commandLine.GetSwitchValue("project-path", 0).c_str(); + } + + ProjectManagerWindow window(nullptr, engineRootPath, projectPath, startScreen); window.show(); // somethings is preventing us from moving the window to the center of the From a3e73948c53820bc70406641eadb8f49a3234332 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:32:57 -0700 Subject: [PATCH 45/53] Improved project creation validation No longer requires project name to be part of the project path. --- AutomatedTesting/preview.png | 4 +- .../Source/CreateProjectCtrl.cpp | 12 +++ .../ProjectManager/Source/CreateProjectCtrl.h | 1 + .../Source/FormLineEditWidget.cpp | 8 ++ .../Source/FormLineEditWidget.h | 1 + .../Source/NewProjectSettingsScreen.cpp | 81 ++++++++++++++----- .../Source/NewProjectSettingsScreen.h | 5 ++ .../ProjectManager/Source/ProjectsScreen.cpp | 10 +++ .../ProjectManager/Source/PythonBindings.cpp | 9 ++- .../ProjectManager/Source/ScreensCtrl.cpp | 27 ++++++- .../Tools/ProjectManager/Source/ScreensCtrl.h | 2 + Templates/DefaultProject/Template/preview.png | 4 +- scripts/o3de/o3de/engine_template.py | 23 ++++-- 13 files changed, 157 insertions(+), 30 deletions(-) diff --git a/AutomatedTesting/preview.png b/AutomatedTesting/preview.png index 3d4fe78063..c6928d31fc 100644 --- a/AutomatedTesting/preview.png +++ b/AutomatedTesting/preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40949893ed7009eeaa90b7ce6057cb6be9dfaf7b162e3c26ba9dadf985939d7d -size 2038 +oid sha256:b9cd9d6f67440c193a85969ec5c082c6343e6d1fff3b6f209a0a6931eb22dd47 +size 2949 diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp index 69f0a3983d..60e351cdb4 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp @@ -67,6 +67,15 @@ namespace O3DE::ProjectManager return ProjectManagerScreen::CreateProject; } + void CreateProjectCtrl::NotifyCurrentScreen() + { + ScreenWidget* currentScreen = reinterpret_cast(m_stack->currentWidget()); + if (currentScreen) + { + currentScreen->NotifyCurrentScreen(); + } + } + void CreateProjectCtrl::HandleBackButton() { if (m_stack->currentIndex() > 0) @@ -110,6 +119,9 @@ namespace O3DE::ProjectManager auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo); if (result.IsSuccess()) { + // automatically register the project + PythonBindingsInterface::Get()->AddProject(m_projectInfo.m_path); + // adding gems is not implemented yet because we don't know what targets to add or how to add them emit ChangeScreenRequest(ProjectManagerScreen::Projects); } diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h index 01e3349b21..355ba3941d 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.h @@ -31,6 +31,7 @@ namespace O3DE::ProjectManager explicit CreateProjectCtrl(QWidget* parent = nullptr); ~CreateProjectCtrl() = default; ProjectManagerScreen GetScreenEnum() override; + void NotifyCurrentScreen() override; protected slots: void HandleBackButton(); diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp index 7ef7e3c7d8..6c08393910 100644 --- a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp @@ -78,6 +78,14 @@ namespace O3DE::ProjectManager m_errorLabel->setText(labelText); } + void FormLineEditWidget::setErrorLabelVisible(bool visible) + { + m_errorLabel->setVisible(visible); + m_frame->setProperty("Valid", !visible); + + refreshStyle(); + } + QLineEdit* FormLineEditWidget::lineEdit() const { return m_lineEdit; diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h index 3094442cbd..76534f46f7 100644 --- a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h @@ -39,6 +39,7 @@ namespace O3DE::ProjectManager //! Set the error message for to display when invalid. void setErrorLabelText(const QString& labelText); + void setErrorLabelVisible(bool visible); //! Returns a pointer to the underlying LineEdit. QLineEdit* lineEdit() const; diff --git a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp index b57a2b35b2..53400b3193 100644 --- a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -49,16 +50,16 @@ namespace O3DE::ProjectManager vLayout->setContentsMargins(0,0,0,0); vLayout->setAlignment(Qt::AlignTop); { - m_projectName = new FormLineEditWidget(tr("Project name"), tr("New Project"), this); - m_projectName->setErrorLabelText( - tr("A project with this name already exists at this location. Please choose a new name or location.")); + const QString defaultName{ "NewProject" }; + const QString defaultPath = QDir::toNativeSeparators(GetDefaultProjectPath() + "/" + defaultName); + + m_projectName = new FormLineEditWidget(tr("Project name"), defaultName, this); + connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath); vLayout->addWidget(m_projectName); - m_projectPath = - new FormBrowseEditWidget(tr("Project Location"), QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), this); + m_projectPath = new FormBrowseEditWidget(tr("Project Location"), defaultPath, this); m_projectPath->lineEdit()->setReadOnly(true); - m_projectPath->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); - m_projectPath->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &NewProjectSettingsScreen::ValidateProjectPath); vLayout->addWidget(m_projectPath); // if we don't use a QFrame we cannot "contain" the widgets inside and move them around @@ -112,17 +113,41 @@ namespace O3DE::ProjectManager this->setLayout(hLayout); } + QString NewProjectSettingsScreen::GetDefaultProjectPath() + { + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + QDir path(QDir::toNativeSeparators(engineInfoResult.GetValue().m_defaultProjectsFolder)); + if (path.exists()) + { + defaultPath = path.absolutePath(); + } + } + return defaultPath; + } + ProjectManagerScreen NewProjectSettingsScreen::GetScreenEnum() { return ProjectManagerScreen::NewProjectSettings; } + void NewProjectSettingsScreen::ValidateProjectPath() + { + Validate(); + } + + void NewProjectSettingsScreen::NotifyCurrentScreen() + { + Validate(); + } ProjectInfo NewProjectSettingsScreen::GetProjectInfo() { ProjectInfo projectInfo; projectInfo.m_projectName = m_projectName->lineEdit()->text(); - projectInfo.m_path = QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + projectInfo.m_projectName); + projectInfo.m_path = m_projectPath->lineEdit()->text(); return projectInfo; } @@ -133,24 +158,44 @@ namespace O3DE::ProjectManager bool NewProjectSettingsScreen::Validate() { - bool projectNameIsValid = true; - if (m_projectName->lineEdit()->text().isEmpty()) - { - projectNameIsValid = false; - } - bool projectPathIsValid = true; if (m_projectPath->lineEdit()->text().isEmpty()) { projectPathIsValid = false; + m_projectPath->setErrorLabelText(tr("Please provide a valid location.")); } - - QDir path(QDir::toNativeSeparators(m_projectPath->lineEdit()->text() + "/" + m_projectName->lineEdit()->text())); - if (path.exists() && !path.isEmpty()) + else { - projectPathIsValid = false; + QDir path(m_projectPath->lineEdit()->text()); + if (path.exists() && !path.isEmpty()) + { + projectPathIsValid = false; + m_projectPath->setErrorLabelText(tr("This folder exists and isn't empty. Please choose a different location.")); + } } + bool projectNameIsValid = true; + if (m_projectName->lineEdit()->text().isEmpty()) + { + projectNameIsValid = false; + m_projectName->setErrorLabelText(tr("Please provide a project name.")); + } + else + { + // this validation should roughly match the utils.validate_identifier which the cli + // uses to validate project names + QRegExp validProjectNameRegex("[A-Za-z][A-Za-z0-9_-]{0,63}"); + const bool result = validProjectNameRegex.exactMatch(m_projectName->lineEdit()->text()); + if (!result) + { + projectNameIsValid = false; + m_projectName->setErrorLabelText(tr("Project names must start with a letter and consist of up to 64 letter, number, '_' or '-' characters")); + } + + } + + m_projectName->setErrorLabelVisible(!projectNameIsValid); + m_projectPath->setErrorLabelVisible(!projectPathIsValid); return projectNameIsValid && projectPathIsValid; } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h index f0e9609fdc..0560f8728d 100644 --- a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.h @@ -36,10 +36,15 @@ namespace O3DE::ProjectManager bool Validate(); + void NotifyCurrentScreen() override; + protected slots: void HandleBrowseButton(); + void ValidateProjectPath(); private: + QString GetDefaultProjectPath(); + FormLineEditWidget* m_projectName; FormBrowseEditWidget* m_projectPath; QButtonGroup* m_projectTemplateButtonGroup; diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp index dd2e411ec5..7b9e3ecb9d 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp @@ -341,6 +341,16 @@ namespace O3DE::ProjectManager } else { + // refresh the projects content by re-creating it for now + if (m_projectsContent) + { + m_stack->removeWidget(m_projectsContent); + m_projectsContent->deleteLater(); + } + + m_projectsContent = CreateProjectsContent(); + + m_stack->addWidget(m_projectsContent); m_stack->setCurrentWidget(m_projectsContent); } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 8db8492cae..c0481d6c87 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -513,10 +513,15 @@ namespace O3DE::ProjectManager { ProjectInfo createdProjectInfo; bool result = ExecuteWithLock([&] { - pybind11::str projectPath = projectInfo.m_path.toStdString(); + pybind11::str projectName = projectInfo.m_projectName.toStdString(); pybind11::str templatePath = projectTemplatePath.toStdString(); - auto createProjectResult = m_engineTemplate.attr("create_project")(projectPath, templatePath); + + auto createProjectResult = m_engineTemplate.attr("create_project")( + projectPath, + projectName, + templatePath + ); if (createProjectResult.cast() == 0) { createdProjectInfo = ProjectInfoFromPath(projectPath); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index 7d31d02f6c..6206d4cee9 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -136,6 +136,7 @@ namespace O3DE::ProjectManager { shouldRestoreCurrentScreen = true; } + int tabIndex = GetScreenTabIndex(screen); // Delete old screen if it exists to start fresh DeleteScreen(screen); @@ -144,11 +145,19 @@ namespace O3DE::ProjectManager ScreenWidget* newScreen = BuildScreen(this, screen); if (newScreen->IsTab()) { - m_tabWidget->addTab(newScreen, newScreen->GetTabText()); + if (tabIndex > -1) + { + m_tabWidget->insertTab(tabIndex, newScreen, newScreen->GetTabText()); + } + else + { + m_tabWidget->addTab(newScreen, newScreen->GetTabText()); + } if (shouldRestoreCurrentScreen) { m_tabWidget->setCurrentWidget(newScreen); m_screenStack->setCurrentWidget(m_tabWidget); + newScreen->NotifyCurrentScreen(); } } else @@ -157,6 +166,7 @@ namespace O3DE::ProjectManager if (shouldRestoreCurrentScreen) { m_screenStack->setCurrentWidget(newScreen); + newScreen->NotifyCurrentScreen(); } } @@ -219,4 +229,19 @@ namespace O3DE::ProjectManager screen->NotifyCurrentScreen(); } } + + int ScreensCtrl::GetScreenTabIndex(ProjectManagerScreen screen) + { + const auto iter = m_screenMap.find(screen); + if (iter != m_screenMap.end()) + { + ScreenWidget* screenWidget = iter.value(); + if (screenWidget->IsTab()) + { + return m_tabWidget->indexOf(screenWidget); + } + } + + return -1; + } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.h b/Code/Tools/ProjectManager/Source/ScreensCtrl.h index 935fc78e25..3b51ed529a 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.h +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.h @@ -51,6 +51,8 @@ namespace O3DE::ProjectManager void TabChanged(int index); private: + int GetScreenTabIndex(ProjectManagerScreen screen); + QStackedWidget* m_screenStack; QHash m_screenMap; QStack m_screenVisitOrder; diff --git a/Templates/DefaultProject/Template/preview.png b/Templates/DefaultProject/Template/preview.png index 3d4fe78063..a3e13481c9 100644 --- a/Templates/DefaultProject/Template/preview.png +++ b/Templates/DefaultProject/Template/preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40949893ed7009eeaa90b7ce6057cb6be9dfaf7b162e3c26ba9dadf985939d7d -size 2038 +oid sha256:4a5881b8d6cfbc4ceefb14ab96844484fe19407ee030824768f9fcce2f729d35 +size 2949 diff --git a/scripts/o3de/o3de/engine_template.py b/scripts/o3de/o3de/engine_template.py index 63dec3e765..9bb62eff35 100755 --- a/scripts/o3de/o3de/engine_template.py +++ b/scripts/o3de/o3de/engine_template.py @@ -1279,6 +1279,7 @@ def create_from_template(destination_path: str, def create_project(project_path: str, + project_name: str = None, template_path: str = None, template_name: str = None, project_restricted_path: str = None, @@ -1297,6 +1298,7 @@ def create_project(project_path: str, Template instantiation specialization that makes all default assumptions for a Project template instantiation, reducing the effort needed in instancing a project :param project_path: the project path, can be absolute or relative to default projects path + :param project_name: the project name, defaults to project_path basename if not provided :param template_path: the path to the template you want to instance, can be absolute or relative to default templates path :param template_name: the name the registered template you want to instance, defaults to DefaultProject, resolves template_path :param project_restricted_path: path to the projects restricted folder, can be absolute or relative to the restricted='projects' @@ -1489,12 +1491,17 @@ def create_project(project_path: str, elif not os.path.isdir(project_path): os.makedirs(project_path) - # project name is now the last component of the project_path - project_name = os.path.basename(project_path) + if not project_name: + # project name is now the last component of the project_path + project_name = os.path.basename(project_path) + + if not utils.validate_identifier(project_name): + logger.error(f'Project name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {project_name}') + return 1 # project name cannot be the same as a restricted platform name if project_name in restricted_platforms: - logger.error(f'Project path cannot be a restricted name. {project_name}') + logger.error(f'Project name cannot be a restricted name. {project_name}') return 1 # project restricted name @@ -2079,6 +2086,7 @@ def _run_create_from_template(args: argparse) -> int: def _run_create_project(args: argparse) -> int: return create_project(args.project_path, + args.project_name, args.template_path, args.template_name, args.project_restricted_path, @@ -2262,10 +2270,15 @@ def add_args(subparsers) -> None: # creation of a project from a template (like create from template but makes project assumptions) create_project_subparser = subparsers.add_parser('create-project') create_project_subparser.add_argument('-pp', '--project-path', type=str, required=True, - help='The name of the project you wish to create from the template,' + help='The location of the project you wish to create from the template,' ' can be an absolute path or dev root relative.' ' Ex. C:/o3de/TestProject' - ' TestProject = ') + ' TestProject = if --project-name not provided') + create_project_subparser.add_argument('-pn', '--project-name', type=str, required=False, + help='The name of the project you wish to use, must be alphanumeric, ' + ' and can contain _ and - characters.' + ' If no name is provided, will use last component of project path.' + ' Ex. New_Project-123') group = create_project_subparser.add_mutually_exclusive_group(required=False) group.add_argument('-tp', '--template-path', type=str, required=False, From 5884fc1096f6742e956d9b309fce1f710711c436 Mon Sep 17 00:00:00 2001 From: michabr <82236305+michabr@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:48:57 -0700 Subject: [PATCH 46/53] Fix LyShine instance not being initialized (#1078) --- Gems/LyShine/Code/Source/LyShineSystemComponent.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp b/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp index 902752a03c..0eab7705f6 100644 --- a/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp +++ b/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp @@ -157,6 +157,7 @@ namespace LyShine UiSystemBus::Handler::BusConnect(); UiSystemToolsBus::Handler::BusConnect(); UiFrameworkBus::Handler::BusConnect(); + CrySystemEventBus::Handler::BusConnect(); // register all the component types internal to the LyShine module // These are registered in the order we want them to appear in the Add Component menu @@ -201,6 +202,7 @@ namespace LyShine UiSystemToolsBus::Handler::BusDisconnect(); UiFrameworkBus::Handler::BusDisconnect(); LyShineRequestBus::Handler::BusDisconnect(); + CrySystemEventBus::Handler::BusDisconnect(); LyShineAllocatorScope::DeactivateAllocators(); } From dfd63737c390090e33fcd02002fdc1f2dd04f623 Mon Sep 17 00:00:00 2001 From: Vincent Liu <5900509+onecent1101@users.noreply.github.com> Date: Tue, 1 Jun 2021 15:59:43 -0700 Subject: [PATCH 47/53] [SPEC-6720] Update session common interfaces (#956) --- .../Session/ISessionHandlingRequests.h | 22 +++++++++++++++---- .../AzFramework/Session/ISessionRequests.h | 3 +++ .../Session/SessionNotifications.h | 3 +++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h index 47388c56c3..a0731626ef 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionHandlingRequests.h @@ -12,6 +12,7 @@ #pragma once +#include #include namespace AzFramework @@ -49,13 +50,17 @@ namespace AzFramework class ISessionHandlingClientRequests { public: - // Handle the player join session process + AZ_RTTI(ISessionHandlingClientRequests, "{41DE6BD3-72BC-4443-BFF9-5B1B9396657A}"); + ISessionHandlingClientRequests() = default; + virtual ~ISessionHandlingClientRequests() = default; + + // Request the player join session // @param sessionConnectionConfig The required properties to handle the player join session process // @return The result of player join session process - virtual bool HandlePlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0; + virtual bool RequestPlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0; - // Handle the player leave session process - virtual void HandlePlayerLeaveSession() = 0; + // Request the connected player leave session + virtual void RequestPlayerLeaveSession() = 0; }; //! ISessionHandlingServerRequests @@ -63,6 +68,10 @@ namespace AzFramework class ISessionHandlingServerRequests { public: + AZ_RTTI(ISessionHandlingServerRequests, "{4F0C17BA-F470-4242-A8CB-EC7EA805257C}"); + ISessionHandlingServerRequests() = default; + virtual ~ISessionHandlingServerRequests() = default; + // Handle the destroy session process virtual void HandleDestroySession() = 0; @@ -74,5 +83,10 @@ namespace AzFramework // Handle the player leave session process // @param playerConnectionConfig The required properties to handle the player leave session process virtual void HandlePlayerLeaveSession(const PlayerConnectionConfig& playerConnectionConfig) = 0; + + // Retrieves the file location of a pem-encoded TLS certificate + // @return If successful, returns the file location of TLS certificate file; if not successful, returns + // empty string. + virtual AZStd::string GetSessionCertificate() = 0; }; } // namespace AzFramework diff --git a/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h index 9d21a7f282..da65eb47f0 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h +++ b/Code/Framework/AzFramework/AzFramework/Session/ISessionRequests.h @@ -167,6 +167,9 @@ namespace AzFramework : public AZ::EBusTraits { public: + // Safeguard handler for multi-threaded use case + using MutexType = AZStd::recursive_mutex; + ////////////////////////////////////////////////////////////////////////// // EBusTraits overrides static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; diff --git a/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h index a61c995db7..c472fcb228 100644 --- a/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h +++ b/Code/Framework/AzFramework/AzFramework/Session/SessionNotifications.h @@ -24,6 +24,9 @@ namespace AzFramework : public AZ::EBusTraits { public: + // Safeguard handler for multi-threaded use case + using MutexType = AZStd::recursive_mutex; + ////////////////////////////////////////////////////////////////////////// // EBusTraits overrides static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; From b3b5864f763cf5bcb9f14a9f783133d411f73f50 Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:59:31 -0500 Subject: [PATCH 48/53] Added key checks for the o3de_manifest.json query functions to avoid python exceptions being raised --- scripts/o3de/o3de/manifest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/o3de/o3de/manifest.py b/scripts/o3de/o3de/manifest.py index 9436e1dd29..2a7e5bba11 100644 --- a/scripts/o3de/o3de/manifest.py +++ b/scripts/o3de/o3de/manifest.py @@ -215,12 +215,12 @@ def get_this_engine() -> dict: def get_engines() -> list: json_data = load_o3de_manifest() - return json_data['engines'] + return json_data['engines'] if 'engines' in json_data else [] def get_projects() -> list: json_data = load_o3de_manifest() - return json_data['projects'] + return json_data['projects'] if 'projects' in json_data else [] def get_gems() -> list: @@ -233,22 +233,22 @@ def get_gems() -> list: def get_external_subdirectories() -> list: json_data = load_o3de_manifest() - return json_data['external_subdirectories'] + return json_data['external_subdirectories'] if 'external_subdirectories' in json_data else [] def get_templates() -> list: json_data = load_o3de_manifest() - return json_data['templates'] + return json_data['templates'] if 'templates' in json_data else [] def get_restricted() -> list: json_data = load_o3de_manifest() - return json_data['restricted'] + return json_data['restricted'] if 'restricted' in json_data else [] def get_repos() -> list: json_data = load_o3de_manifest() - return json_data['repos'] + return json_data['repos'] if 'repos' in json_data else [] # engine.json queries def get_engine_projects() -> list: From 810d6a8deb2da1dbdbe365e3f42cc240c2c7231e Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:04:08 -0500 Subject: [PATCH 49/53] Moved the add_gem_dependency and remove_gem_dependency methods to the cmake.py file Fixed the add_gem_dependency method to append the newly added gem right after the 'set(ENABLED_GEMS...' line --- scripts/o3de/o3de/cmake.py | 91 +++++++++++++++++++++++++++++++- scripts/o3de/o3de/disable_gem.py | 36 +------------ scripts/o3de/o3de/enable_gem.py | 54 +------------------ 3 files changed, 92 insertions(+), 89 deletions(-) diff --git a/scripts/o3de/o3de/cmake.py b/scripts/o3de/o3de/cmake.py index dfcce708eb..f8e8d6ce0c 100644 --- a/scripts/o3de/o3de/cmake.py +++ b/scripts/o3de/o3de/cmake.py @@ -21,6 +21,95 @@ from o3de import manifest logger = logging.getLogger() logging.basicConfig() +enable_gem_start_marker = 'set(ENABLED_GEMS' +enable_gem_end_marker = ')' + + +def add_gem_dependency(cmake_file: pathlib.Path, + gem_name: str) -> int: + """ + adds a gem dependency to a cmake file + :param cmake_file: path to the cmake file + :param gem_name: name of the gem + :return: 0 for success or non 0 failure code + """ + if not cmake_file.is_file(): + logger.error(f'Failed to locate cmake file {str(cmake_file)}') + return 1 + + # on a line by basis, see if there already is {gem_name} + # find the first occurrence of a gem, copy its formatting and replace + # the gem name with the new one and append it + # if the gem is already present fail + t_data = [] + added = False + line_index_to_append = None + with open(cmake_file, 'r') as s: + line_index = 0 + for line in s: + if line.strip().startswith(enable_gem_start_marker): + line_index_to_append = line_index + if f'{gem_name}' == line.strip(): + logger.warning(f'{gem_name} is already enabled in file {str(cmake_file)}.') + return 0 + t_data.append(line) + line_index += 1 + + + indent = 4 + if line_index_to_append: + # Insert the gem after the 'set(ENABLED_GEMS)...` line + t_data.insert(line_index_to_append + 1, f'{" " * indent}{gem_name}\n') + added = True + + # if we didn't add, then create a new set(ENABLED_GEMS) variable + # add a new gem, if empty the correct format is 1 tab=4spaces + if not added: + t_data.append('\n') + t_data.append(f'{enable_gem_start_marker}\n') + t_data.append(f'{" " * indent}{gem_name}\n') + t_data.append(f'{enable_gem_end_marker}\n') + + # write the cmake + with open(cmake_file, 'w') as s: + s.writelines(t_data) + + return 0 + +def remove_gem_dependency(cmake_file: pathlib.Path, + gem_name: str) -> int: + """ + removes a gem dependency from a cmake file + :param cmake_file: path to the cmake file + :param gem_name: name of the gem + :return: 0 for success or non 0 failure code + """ + if not cmake_file.is_file(): + logger.error(f'Failed to locate cmake file {cmake_file}') + return 1 + + # on a line by basis, remove any line with {gem_name} + t_data = [] + # Remove the gem from the enabled_gem file by skipping the gem name entry + removed = False + with open(cmake_file, 'r') as s: + for line in s: + if gem_name == line.strip(): + removed = True + else: + t_data.append(line) + + if not removed: + logger.error(f'Failed to remove {gem_name} from cmake file {cmake_file}') + return 1 + + # write the cmake + with open(cmake_file, 'w') as s: + s.writelines(t_data) + + return 0 + + def get_project_gems(project_path: pathlib.Path, platform: str = 'Common') -> set: return get_gems_from_cmake_file(get_enabled_gem_cmake_file(project_path=project_path, platform=platform)) @@ -38,8 +127,6 @@ def get_enabled_gems(cmake_file: pathlib.Path) -> set: logger.error(f'Failed to locate cmake file {cmake_file}') return set() - enable_gem_start_marker = 'set(ENABLED_GEMS' - enable_gem_end_marker = ')' gem_target_set = set() with cmake_file.open('r') as s: in_gem_list = False diff --git a/scripts/o3de/o3de/disable_gem.py b/scripts/o3de/o3de/disable_gem.py index 61d71445f0..82fe9c8ac6 100644 --- a/scripts/o3de/o3de/disable_gem.py +++ b/scripts/o3de/o3de/disable_gem.py @@ -24,40 +24,6 @@ logger = logging.getLogger() logging.basicConfig() -def remove_gem_dependency(cmake_file: pathlib.Path, - gem_name: str) -> int: - """ - removes a gem dependency from a cmake file - :param cmake_file: path to the cmake file - :param gem_name: name of the gem - :return: 0 for success or non 0 failure code - """ - if not cmake_file.is_file(): - logger.error(f'Failed to locate cmake file {cmake_file}') - return 1 - - # on a line by basis, remove any line with {gem_name} - t_data = [] - # Remove the gem from the enabled_gem file by skipping the gem name entry - removed = False - with open(cmake_file, 'r') as s: - for line in s: - if gem_name == line.strip(): - removed = True - else: - t_data.append(line) - - if not removed: - logger.error(f'Failed to remove {gem_name} from cmake file {cmake_file}') - return 1 - - # write the cmake - with open(cmake_file, 'w') as s: - s.writelines(t_data) - - return 0 - - def disable_gem_in_project(gem_name: str = None, gem_path: pathlib.Path = None, project_name: str = None, @@ -128,7 +94,7 @@ def disable_gem_in_project(gem_name: str = None, logger.error(f'Enabled gem file {enabled_gem_file} is not present.') return 1 # remove the gem - error_code = remove_gem_dependency(enabled_gem_file, gem_json_data['gem_name']) + error_code = cmake.remove_gem_dependency(enabled_gem_file, gem_json_data['gem_name']) if error_code: ret_val = error_code diff --git a/scripts/o3de/o3de/enable_gem.py b/scripts/o3de/o3de/enable_gem.py index 0dee01e05e..0e007f6370 100644 --- a/scripts/o3de/o3de/enable_gem.py +++ b/scripts/o3de/o3de/enable_gem.py @@ -24,56 +24,6 @@ from o3de import cmake, manifest, validation logger = logging.getLogger() logging.basicConfig() -def add_gem_dependency(cmake_file: pathlib.Path, - gem_name: str) -> int: - """ - adds a gem dependency to a cmake file - :param cmake_file: path to the cmake file - :param gem_name: name of the gem - :return: 0 for success or non 0 failure code - """ - if not cmake_file.is_file(): - logger.error(f'Failed to locate cmake file {str(cmake_file)}') - return 1 - - # on a line by basis, see if there already is {gem_name} - # find the first occurrence of a gem, copy its formatting and replace - # the gem name with the new one and append it - # if the gem is already present fail - t_data = [] - added = False - line_index_to_append = None - with open(cmake_file, 'r') as s: - line_index = 0 - for line in s: - if 'ENABLED_GEMS' in line: - line_index_to_append = line_index - if f'{gem_name}' == line.strip(): - logger.warning(f'{gem_name} is already enabled in file {str(cmake_file)}.') - return 0 - t_data.append(line) - line_index += 1 - - - indent = 4 - if line_index_to_append: - t_data[line_index_to_append] = f'{" " * indent}{gem_name}\n' - added = True - - # if we didn't add, then create a new set(ENABLED_GEMS) variable - # add a new gem, if empty the correct format is 1 tab=4spaces - if not added: - t_data.append('\n') - t_data.append('set(ENABLED_GEMS\n') - t_data.append(f'{" " * indent}{gem_name}\n') - t_data.append(')\n') - - # write the cmake - with open(cmake_file, 'w') as s: - s.writelines(t_data) - - return 0 - def enable_gem_in_project(gem_name: str = None, gem_path: pathlib.Path = None, @@ -141,7 +91,7 @@ def enable_gem_in_project(gem_name: str = None, logger.error(f'Enabled gem file {enabled_gem_file} is not present.') return 1 # add the gem - ret_val = add_gem_dependency(enabled_gem_file, gem_json_data['gem_name']) + ret_val = cmake.add_gem_dependency(enabled_gem_file, gem_json_data['gem_name']) else: # Find the path to enabled gem file. @@ -150,7 +100,7 @@ def enable_gem_in_project(gem_name: str = None, if not project_enabled_gem_file.is_file(): project_enabled_gem_file.touch() # add the gem - ret_val = add_gem_dependency(project_enabled_gem_file, gem_json_data['gem_name']) + ret_val = cmake.add_gem_dependency(project_enabled_gem_file, gem_json_data['gem_name']) return ret_val From bf0df4b36962fa7dcbbe5b62d59e8e65ace2b5ca Mon Sep 17 00:00:00 2001 From: Steve Pham <82231385+spham-amzn@users.noreply.github.com> Date: Tue, 1 Jun 2021 16:37:30 -0700 Subject: [PATCH 50/53] Add Android 'gradle' job as a default job (#1082) * Add Android 'gradle' job as a default job * Replace warning about version checking type with string preprocessing of the captured version before comparisons --- cmake/Tools/common.py | 6 +++++- scripts/build/Platform/Android/build_config.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmake/Tools/common.py b/cmake/Tools/common.py index 9c0d31cd53..1990041c86 100755 --- a/cmake/Tools/common.py +++ b/cmake/Tools/common.py @@ -316,7 +316,11 @@ def verify_tool(override_tool_path, tool_name, tool_filename, argument_name, too version_match = tool_version_regex.search(version_output) if not version_match: raise RuntimeError() - result_version = LooseVersion(str(version_match.group(1)).strip()) + + + # Since we are doing a compare, strip out any non-numeric and non . character from the version otherwise we will get a TypeError on the LooseVersion comparison + result_version_str = re.sub(r"[^\.0-9]", "", str(version_match.group(1)).strip()) + result_version = LooseVersion(result_version_str) if min_version and result_version < min_version: raise LmbrCmdError(f"The {tool_desc} does not meet the minimum version of {tool_name} required ({str(min_version)}).", diff --git a/scripts/build/Platform/Android/build_config.json b/scripts/build/Platform/Android/build_config.json index b871670cd0..33248ffe26 100644 --- a/scripts/build/Platform/Android/build_config.json +++ b/scripts/build/Platform/Android/build_config.json @@ -136,6 +136,7 @@ }, "gradle": { "TAGS":[ + "default", "weekly-build-metrics" ], "COMMAND":"gradle_windows.cmd", From eab3db3d6dbdfdf9d1d179470e8e889c343ebfe1 Mon Sep 17 00:00:00 2001 From: Terry Michaels Date: Tue, 1 Jun 2021 20:19:12 -0500 Subject: [PATCH 51/53] Fixed a few 'too big' dialog issues with various dialogs (#1083) --- Code/Sandbox/Editor/CryEdit.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Code/Sandbox/Editor/CryEdit.cpp b/Code/Sandbox/Editor/CryEdit.cpp index c60f32560b..c723e6049a 100644 --- a/Code/Sandbox/Editor/CryEdit.cpp +++ b/Code/Sandbox/Editor/CryEdit.cpp @@ -281,6 +281,8 @@ BOOL CCryDocManager::DoPromptFileName(QString& fileName, [[maybe_unused]] UINT n [[maybe_unused]] DWORD lFlags, BOOL bOpenFileDialog, [[maybe_unused]] CDocTemplate* pTemplate) { CLevelFileDialog levelFileDialog(bOpenFileDialog); + levelFileDialog.show(); + levelFileDialog.adjustSize(); if (levelFileDialog.exec() == QDialog::Accepted) { @@ -2079,6 +2081,8 @@ void CCryEditApp::OnDocumentationAWSSupport() void CCryEditApp::OnDocumentationFeedback() { FeedbackDialog dialog; + dialog.show(); + dialog.adjustSize(); dialog.exec(); } @@ -3347,6 +3351,8 @@ void CCryEditApp::OnCreateSlice() void CCryEditApp::OnOpenLevel() { CLevelFileDialog levelFileDialog(true); + levelFileDialog.show(); + levelFileDialog.adjustSize(); if (levelFileDialog.exec() == QDialog::Accepted) { From 262c1c1132b153093c863c50945edf4ce128e1f2 Mon Sep 17 00:00:00 2001 From: Mike Chang <62353586+amzn-changml@users.noreply.github.com> Date: Tue, 1 Jun 2021 19:14:13 -0700 Subject: [PATCH 52/53] Change node label for Mac/iOS for new AMI update (#1086) Changes the default node label for Mac/iOS to the newest AMI - This AMI contains updates for XCode and CMake - CMake is now on 3.20.2 --- scripts/build/Platform/Mac/pipeline.json | 2 +- scripts/build/Platform/iOS/pipeline.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build/Platform/Mac/pipeline.json b/scripts/build/Platform/Mac/pipeline.json index 58f62b421d..81c57002b6 100644 --- a/scripts/build/Platform/Mac/pipeline.json +++ b/scripts/build/Platform/Mac/pipeline.json @@ -1,6 +1,6 @@ { "ENV": { - "NODE_LABEL": "mac", + "NODE_LABEL": "mac-catalina-7ad2e45b", "LY_3RDPARTY_PATH": "/Users/lybuilder/3rdParty", "TIMEOUT": 30, "WORKSPACE": "/Users/lybuilder/workspace", diff --git a/scripts/build/Platform/iOS/pipeline.json b/scripts/build/Platform/iOS/pipeline.json index 58f62b421d..81c57002b6 100644 --- a/scripts/build/Platform/iOS/pipeline.json +++ b/scripts/build/Platform/iOS/pipeline.json @@ -1,6 +1,6 @@ { "ENV": { - "NODE_LABEL": "mac", + "NODE_LABEL": "mac-catalina-7ad2e45b", "LY_3RDPARTY_PATH": "/Users/lybuilder/3rdParty", "TIMEOUT": 30, "WORKSPACE": "/Users/lybuilder/workspace", From 0f699cfd470701f21dc6c2e498e9cde9dfa5361d Mon Sep 17 00:00:00 2001 From: Benjamin Jillich <43751992+amzn-jillich@users.noreply.github.com> Date: Wed, 2 Jun 2021 08:35:59 +0200 Subject: [PATCH 53/53] [LYN-4157] EMotionFX: Adding/removing colliders to/from ragdoll and saving the actor crashes the Editor (#1055) The actor dirty flag was set to true even after saving the asset info which resulted in the save dirty files dialog to appear providing the user to save the actor another time, just after saving it which is confusing. This might have led to rendering an already deleted actor and the crash. Though, I was not able to stably reproduce the issue and can't reproduce it anymore after this fix. --- .../Tools/EMotionStudio/EMStudioSDK/Source/Commands.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/Commands.cpp b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/Commands.cpp index 294f4c7c99..46760af8f0 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/Commands.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/Commands.cpp @@ -218,6 +218,10 @@ namespace EMStudio } const bool saveResult = manifest.SaveToFile(manifestFilename.c_str()); + if (saveResult) + { + actor->SetDirtyFlag(false); + } // Source Control: Add file in case it did not exist before (when saving it the first time). if (saveResult && !fileExisted)