From 5ff65be3145c43215c4a9e635316016919bdcbfa Mon Sep 17 00:00:00 2001 From: nemerle <96597+nemerle@users.noreply.github.com> Date: Sun, 12 Dec 2021 23:48:47 +0100 Subject: [PATCH 01/37] This reduces non-unity build time by ~2% and build size by ~0.5%. This PR is a 'clean' version of #6199 updated to latest development Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> --- Code/Editor/GameEngine.cpp | 1 + .../AtomCore/Instance/InstanceDatabase.h | 5 + .../AzCore/AzCore/Asset/AssetCommon.h | 2 +- .../AzCore/AzCore/Asset/AssetDataStream.cpp | 98 +++- .../AzCore/AzCore/Asset/AssetDataStream.h | 35 +- .../AzCore/AzCore/Asset/AssetManager.cpp | 1 + .../AzCore/AzCore/Asset/AssetManager.h | 1 - Code/Framework/AzCore/AzCore/IO/IStreamer.h | 6 +- .../AzCore/AzCore/IO/Streamer/BlockCache.cpp | 14 +- .../AzCore/AzCore/IO/Streamer/BlockCache.h | 8 +- .../AzCore/IO/Streamer/DedicatedCache.cpp | 20 +- .../AzCore/IO/Streamer/DedicatedCache.h | 23 +- .../AzCore/AzCore/IO/Streamer/FileRequest.cpp | 143 ++++-- .../AzCore/AzCore/IO/Streamer/FileRequest.h | 485 ++++++++++-------- .../IO/Streamer/FullFileDecompressor.cpp | 46 +- .../AzCore/IO/Streamer/FullFileDecompressor.h | 7 +- .../AzCore/IO/Streamer/ReadSplitter.cpp | 10 +- .../AzCore/AzCore/IO/Streamer/Scheduler.cpp | 50 +- .../AzCore/AzCore/IO/Streamer/Scheduler.h | 19 +- .../AzCore/IO/Streamer/StorageDrive.cpp | 42 +- .../AzCore/AzCore/IO/Streamer/StorageDrive.h | 8 +- .../AzCore/AzCore/IO/Streamer/Streamer.cpp | 7 +- .../AzCore/AzCore/IO/Streamer/Streamer.h | 8 +- .../AzCore/IO/Streamer/StreamerComponent.cpp | 3 +- .../AzCore/IO/Streamer/StreamerContext.cpp | 6 +- .../AzCore/IO/Streamer/StreamerContext.h | 15 +- .../IO/Streamer/StorageDrive_Windows.cpp | 52 +- .../AzCore/IO/Streamer/StorageDrive_Windows.h | 11 +- .../Tests/Asset/AssetDataStreamTests.cpp | 1 + .../IO/Streamer/StorageDriveTests_Windows.cpp | 26 +- .../AzCore/Tests/Streamer/BlockCacheTests.cpp | 16 +- .../Tests/Streamer/FullDecompressorTests.cpp | 2 +- .../AzCore/Tests/Streamer/IStreamerMock.h | 1 + .../Tests/Streamer/ReadSplitterTests.cpp | 10 +- .../AzCore/Tests/Streamer/SchedulerTests.cpp | 7 +- .../StreamStackEntryConformityTests.h | 1 + Code/Framework/AzCore/Tests/StreamerTests.cpp | 1 + .../Asset/AssetSystemComponent.cpp | 1 + .../AzFramework/IO/RemoteStorageDrive.cpp | 42 +- .../AzFramework/IO/RemoteStorageDrive.h | 11 +- .../Physics/Common/PhysicsSimulatedBody.cpp | 1 + .../Common/PhysicsSimulatedBodyAutomation.cpp | 1 + .../Common/PhysicsSimulatedBodyEvents.cpp | 1 + .../AzFramework/Physics/PhysicsScene.cpp | 1 + .../AzFramework/Physics/PhysicsSystem.cpp | 1 + .../AzFramework/Script/ScriptComponent.cpp | 1 + .../ToolsAssetCatalogComponent.h | 1 + .../Model/AssetCompleterModel.h | 5 +- .../AssetBuilder/AssetBuilderComponent.cpp | 1 + .../Shader/ShaderVariantAsyncLoader.h | 6 +- .../Shader/PrecompiledShaderAssetSourceData.h | 1 + .../RPI/Code/Source/RPI.Public/Culling.cpp | 24 +- .../Shader/Metrics/ShaderMetricsSystem.cpp | 4 +- .../Tests/Common/AssetManagerTestFixture.cpp | 2 + .../Atom/Utils/AssetCollectionAsyncLoader.h | 1 + .../Code/Source/AtomActorInstance.cpp | 2 + .../Code/Rendering/SharedBuffer.cpp | 2 + .../Code/Source/Engine/ATLEntities.cpp | 18 + .../Code/Source/Engine/ATLEntities.h | 14 +- .../Code/Source/Engine/FileCacheManager.cpp | 1 + .../Code/Tests/AudioSystemTest.cpp | 1 + .../Code/Tests/Mocks/FileCacheManagerMock.h | 5 + .../Code/EMotionFX/Source/ActorInstance.cpp | 2 + .../Code/Tests/TestAssetCode/SimpleActors.cpp | 2 + .../Asset/AssetSystemDebugComponent.cpp | 2 + .../Audio/AudioAreaEnvironmentComponent.cpp | 1 + .../ClothComponentMesh/ActorClothColliders.h | 1 + Gems/NvCloth/Code/Source/Utils/AssetHelper.h | 1 + .../Joints/JointsSubComponentModeAngleCone.h | 1 + .../Joints/JointsSubComponentModeSnap.h | 1 + .../Code/Source/ForceRegionComponent.cpp | 2 + .../Code/Source/Joint/PhysXJointUtils.cpp | 14 +- .../PhysX/Code/Source/Joint/PhysXJointUtils.h | 7 + Gems/PhysX/Code/Source/JointComponent.cpp | 11 +- Gems/PhysX/Code/Source/Material.cpp | 3 +- .../PhysXCharacters/API/CharacterUtils.cpp | 11 +- .../PhysXCharacters/API/RagdollNode.cpp | 11 +- .../Pipeline/HeightFieldAssetHandler.cpp | 3 +- .../Code/Source/Pipeline/StreamWrapper.h | 1 + Gems/PhysX/Code/Source/Scene/PhysXScene.cpp | 18 +- Gems/PhysX/Code/Source/System/PhysXSystem.cpp | 18 +- Gems/PhysX/Code/Tests/PhysXTestUtil.cpp | 1 + .../Code/Source/SystemComponent.cpp | 23 +- .../Editor/Assets/ScriptCanvasMemoryAsset.cpp | 9 +- .../Windows/Tools/UpgradeTool/FileSaver.cpp | 2 + .../Code/Source/ScriptEventsSystemComponent.h | 1 + .../WhiteBoxVertexTranslationModifier.h | 1 + 87 files changed, 882 insertions(+), 604 deletions(-) diff --git a/Code/Editor/GameEngine.cpp b/Code/Editor/GameEngine.cpp index db4c587f54..3753b5fa37 100644 --- a/Code/Editor/GameEngine.cpp +++ b/Code/Editor/GameEngine.cpp @@ -18,6 +18,7 @@ // AzCore #include #include +#include #include #include diff --git a/Code/Framework/AtomCore/AtomCore/Instance/InstanceDatabase.h b/Code/Framework/AtomCore/AtomCore/Instance/InstanceDatabase.h index 4b4ad572c2..de98ce97b9 100644 --- a/Code/Framework/AtomCore/AtomCore/Instance/InstanceDatabase.h +++ b/Code/Framework/AtomCore/AtomCore/Instance/InstanceDatabase.h @@ -17,6 +17,11 @@ #include #include +namespace AZStd +{ + class any; +} + namespace AZ { namespace Data diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h index e4d2c7612e..697ca81a49 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h @@ -19,7 +19,7 @@ #include #include #include -#include +#include namespace AZ { diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.cpp b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.cpp index 4794b04626..7c9593fe3d 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.cpp +++ b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.cpp @@ -7,11 +7,61 @@ */ #include +#include +#include +#include +#include +#include + +#include +#include namespace AZ::Data { + namespace Internal + { + struct AssetDataStreamPrivate + { + //! Optional data buffer that's been directly passed in through Open(), instead of reading data from a file. + AZStd::vector m_preloadedData; + //! The current active streamer read request - tracked in case we need to cancel it prematurely + AZ::IO::FileRequestPtr m_curReadRequest{ nullptr }; + + //! Synchronization for the read request, so that it's possible to block until completion. + AZStd::mutex m_readRequestMutex; + AZStd::condition_variable m_readRequestActive; + + void SetReadRequest(AZ::IO::FileRequestPtr&& req) + { + AZStd::scoped_lock lock(m_readRequestMutex); + // The read request finished, so stop tracking it. + m_curReadRequest = AZStd::move(req); + } + void BlockUntilReadComplete() + { + AZStd::unique_lock lock(m_readRequestMutex); + m_readRequestActive.wait( + lock, + [this] + { + return m_curReadRequest == nullptr; + }); + lock.unlock(); + } + void CancelRequest() + { + AZStd::scoped_lock lock(m_readRequestMutex); + if (m_curReadRequest) + { + auto streamer = Interface::Get(); + m_curReadRequest = streamer->Cancel(m_curReadRequest); + } + } + }; + } // namespace Internal AssetDataStream::AssetDataStream(AZ::IO::IStreamerTypes::RequestMemoryAllocator* bufferAllocator) : m_bufferAllocator(bufferAllocator ? bufferAllocator : &m_defaultAllocator) + , m_privateData(new Internal::AssetDataStreamPrivate) { ClearInternalStateData(); } @@ -22,6 +72,7 @@ namespace AZ::Data { Close(); } + delete m_privateData; } @@ -53,9 +104,9 @@ namespace AZ::Data OpenInternal(data.size(), "(mem buffer)"); // Directly take ownership of the provided buffer - m_preloadedData = AZStd::move(data); - m_buffer = m_preloadedData.data(); - m_loadedSize = m_preloadedData.size(); + m_privateData->m_preloadedData = AZStd::move(data); + m_buffer = m_privateData->m_preloadedData.data(); + m_loadedSize = m_privateData->m_preloadedData.size(); } void AssetDataStream::Open(const AZStd::string& filePath, size_t fileOffset, size_t assetSize, @@ -65,7 +116,7 @@ namespace AZ::Data AZ_PROFILE_FUNCTION(AzCore); AZ_Assert(!m_isOpen, "Attempting to open the stream when it is already open."); - AZ_Assert(!m_curReadRequest, "Queueing an asset stream load while one is still in progress."); + AZ_Assert(!m_privateData->m_curReadRequest, "Queueing an asset stream load while one is still in progress."); AZ_Assert(!filePath.empty(), "AssetDataStream::Open called without a valid file name."); // Initialize the state variables and start tracking the overall load timings @@ -97,11 +148,8 @@ namespace AZ::Data "Buffer for %s was expected to be %zu bytes, but is %zu bytes.", m_filePath.c_str(), m_requestedAssetSize, m_loadedSize); - { - AZStd::scoped_lock lock(m_readRequestMutex); - // The read request finished, so stop tracking it. - m_curReadRequest = nullptr; - } + // The read request finished, so stop tracking it. + m_privateData->SetReadRequest(nullptr); // Call the load callback to start processing the loaded data. if (loadCallback) @@ -115,21 +163,22 @@ namespace AZ::Data } // Notify that the load is complete, in case anyone is using BlockUntilLoadComplete to block. - m_readRequestActive.notify_one(); + m_privateData->m_readRequestActive.notify_one(); }; // Queue the raw file load with the file streamer. auto streamer = AZ::Interface::Get(); - m_curReadRequest = streamer->Read( + m_privateData->m_curReadRequest = + streamer->Read( m_filePath, *m_bufferAllocator, m_requestedAssetSize, deadline, priority, m_fileOffset); m_curDeadline = deadline; m_curPriority = priority; - streamer->SetRequestCompleteCallback(m_curReadRequest, streamerCallback); + streamer->SetRequestCompleteCallback(m_privateData->m_curReadRequest, streamerCallback); - streamer->QueueRequest(m_curReadRequest); + streamer->QueueRequest(m_privateData->m_curReadRequest); } else { @@ -139,19 +188,19 @@ namespace AZ::Data loadCallback(AZ::IO::IStreamerTypes::RequestStatus::Completed); } - m_readRequestActive.notify_one(); + m_privateData->m_readRequestActive.notify_one(); } } void AssetDataStream::Reschedule(AZStd::chrono::milliseconds newDeadline, AZ::IO::IStreamerTypes::Priority newPriority) { - if (m_curReadRequest && (newDeadline < m_curDeadline || newPriority > m_curPriority)) + if (m_privateData->m_curReadRequest && (newDeadline < m_curDeadline || newPriority > m_curPriority)) { auto deadline = AZStd::GetMin(m_curDeadline, newDeadline); auto priority = AZStd::GetMax(m_curPriority, newPriority); auto streamer = Interface::Get(); - m_curReadRequest = streamer->RescheduleRequest(m_curReadRequest, deadline, priority); + m_privateData->m_curReadRequest = streamer->RescheduleRequest(m_privateData->m_curReadRequest, deadline, priority); m_curDeadline = deadline; m_curPriority = priority; } @@ -159,15 +208,13 @@ namespace AZ::Data void AssetDataStream::BlockUntilLoadComplete() { - AZStd::unique_lock lock(m_readRequestMutex); - m_readRequestActive.wait(lock, [this] { return m_curReadRequest == nullptr; }); - lock.unlock(); + m_privateData->BlockUntilReadComplete(); } void AssetDataStream::ClearInternalStateData() { // Clear all our internal state data. - m_preloadedData.resize(0); + m_privateData->m_preloadedData.resize(0); m_buffer = nullptr; m_loadedSize = 0; m_requestedAssetSize = 0; @@ -204,10 +251,10 @@ namespace AZ::Data void AssetDataStream::Close() { AZ_Assert(m_isOpen, "Attempting to close a stream that hasn't been opened."); - AZ_Assert(m_curReadRequest == nullptr, "Attempting to close a stream with a read request in flight."); + AZ_Assert(m_privateData->m_curReadRequest == nullptr, "Attempting to close a stream with a read request in flight."); // Destroy the asset buffer and unlock the allocator, so the allocator itself knows that it is no longer needed. - if (m_buffer != m_preloadedData.data()) + if (m_buffer != m_privateData->m_preloadedData.data()) { m_bufferAllocator->Release(m_buffer); } @@ -221,12 +268,7 @@ namespace AZ::Data void AssetDataStream::RequestCancel() { - AZStd::scoped_lock lock(m_readRequestMutex); - if (m_curReadRequest) - { - auto streamer = Interface::Get(); - m_curReadRequest = streamer->Cancel(m_curReadRequest); - } + m_privateData->CancelRequest(); } void AssetDataStream::Seek(AZ::IO::OffsetType bytes, AZ::IO::GenericStream::SeekMode mode) diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h index 79822c8db2..f095e31562 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetDataStream.h @@ -9,17 +9,26 @@ #include #include -#include -#include -#include -#include -#include +#include + + +namespace AZStd +{ + template + class vector; +} namespace AZ::Data { + namespace Internal + { + struct AssetDataStreamPrivate; + } + class AssetDataStream : public AZ::IO::GenericStream { public: + using VectorDataSource = AZStd::vector; // The default Generic Stream APIs in this class will only allow for a single sequential pass // through the data, no seeking. Reads will block when pages aren't available yet, and // pages will be marked for recycling once reading has progressed beyond them. @@ -29,10 +38,10 @@ namespace AZ::Data ~AssetDataStream() override; // Open the AssetDataStream and make a copy of the provided memory buffer. - void Open(const AZStd::vector& data); + void Open(const VectorDataSource& data); // Open the AssetDataStream and directly take ownership of a pre-populated memory buffer. - void Open(AZStd::vector&& data); + void Open(VectorDataSource&& data); // Open the AssetDataStream and load it via file streaming using OnCompleteCallback = AZStd::function; @@ -91,6 +100,8 @@ namespace AZ::Data void ClearInternalStateData(); + Internal::AssetDataStreamPrivate* m_privateData; + //! The allocator to use for allocating / deallocating asset buffers AZ::IO::IStreamerTypes::RequestMemoryAllocator* m_bufferAllocator{ nullptr }; @@ -106,9 +117,6 @@ namespace AZ::Data //! The amount of data that's expected to be loaded. size_t m_requestedAssetSize{ 0 }; - //! Optional data buffer that's been directly passed in through Open(), instead of reading data from a file. - AZStd::vector m_preloadedData; - //! The buffer that will hold the raw data after it's loaded from the file. void* m_buffer{ nullptr }; @@ -119,19 +127,12 @@ namespace AZ::Data //! The current offset representing how far we've read into the buffer. size_t m_curOffset{ 0 }; - //! The current active streamer read request - tracked in case we need to cancel it prematurely - AZ::IO::FileRequestPtr m_curReadRequest{ nullptr }; - //! The current request deadline. Used to avoid requesting a reschedule to the same (current) deadline. AZStd::chrono::milliseconds m_curDeadline{ AZ::IO::IStreamerTypes::s_noDeadline }; //! The current request priority. Used to avoid requesting a reschedule to the same (current) priority. AZ::IO::IStreamerTypes::Priority m_curPriority{ AZ::IO::IStreamerTypes::s_priorityMedium }; - //! Synchronization for the read request, so that it's possible to block until completion. - AZStd::mutex m_readRequestMutex; - AZStd::condition_variable m_readRequestActive; - //! Track whether or not the stream is currently open bool m_isOpen{ false }; diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp b/Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp index afe3330bd3..b4b1a0d81c 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp +++ b/Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetManager.h b/Code/Framework/AzCore/AzCore/Asset/AssetManager.h index 9666d434c1..663de2c058 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetManager.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetManager.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #include // used as allocator for most components #include diff --git a/Code/Framework/AzCore/AzCore/IO/IStreamer.h b/Code/Framework/AzCore/AzCore/IO/IStreamer.h index 438384e687..d59ee5007e 100644 --- a/Code/Framework/AzCore/AzCore/IO/IStreamer.h +++ b/Code/Framework/AzCore/AzCore/IO/IStreamer.h @@ -15,14 +15,18 @@ #include #include #include +#include // These Streamer includes need to be moved to Streamer internals/implementation, // and pull out only what we need for visibility at IStreamer.h interface declaration. #include -#include namespace AZ::IO { + class ExternalFileRequest; + class FileRequestHandle; + + using FileRequestPtr = AZStd::intrusive_ptr; /** * Data Streamer Interface */ diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.cpp index 6c873f3050..ca38414eaa 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.cpp @@ -137,18 +137,18 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { ReadFile(request, args); return; } else { - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { FlushCache(args.m_path); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushEntireCache(); } @@ -166,7 +166,7 @@ namespace AZ::IO { Section& delayed = m_delayedSections.front(); AZ_Assert(delayed.m_parent, "Delayed section doesn't have a reference to the original request."); - auto data = AZStd::get_if(&delayed.m_parent->GetCommand()); + auto data = AZStd::get_if(&delayed.m_parent->GetCommand()); AZ_Assert(data, "A request in the delayed queue of the BlockCache didn't have a parent with read data."); // This call can add the same section to the back of the queue if there's not // enough space. Because of this the entry needs to be removed from the delayed @@ -233,7 +233,7 @@ namespace AZ::IO } } - void BlockCache::ReadFile(FileRequest* request, FileRequest::ReadData& data) + void BlockCache::ReadFile(FileRequest* request, Requests::ReadData& data) { if (!m_next) { @@ -250,7 +250,7 @@ namespace AZ::IO m_numMetaDataRetrievalInProgress--; if (fileSizeRequest.GetStatus() == IStreamerTypes::RequestStatus::Completed) { - auto& requestInfo = AZStd::get(fileSizeRequest.GetCommand()); + auto& requestInfo = AZStd::get(fileSizeRequest.GetCommand()); if (requestInfo.m_found) { ContinueReadFile(request, requestInfo.m_fileSize); @@ -272,7 +272,7 @@ namespace AZ::IO Section main; Section epilog; - auto& data = AZStd::get(request->GetCommand()); + auto& data = AZStd::get(request->GetCommand()); if (!SplitRequest(prolog, main, epilog, data.m_path, fileLength, data.m_offset, data.m_size, reinterpret_cast(data.m_output))) diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.h b/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.h index 90fb7ea193..68aa2da689 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/BlockCache.h @@ -21,6 +21,12 @@ namespace AZ::IO { + class RequestPath; + namespace Requests + { + struct ReadData; + } + struct BlockCacheConfig final : public IStreamerStackConfig { @@ -109,7 +115,7 @@ namespace AZ::IO using TimePoint = AZStd::chrono::system_clock::time_point; - void ReadFile(FileRequest* request, FileRequest::ReadData& data); + void ReadFile(FileRequest* request, Requests::ReadData& data); void ContinueReadFile(FileRequest* request, u64 fileLength); CacheResult ReadFromCache(FileRequest* request, Section& section, const RequestPath& filePath); CacheResult ReadFromCache(FileRequest* request, Section& section, u32 cacheBlock); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.cpp index b80a1ea724..f155d26eef 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.cpp @@ -101,12 +101,12 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { args.m_range = FileRange::CreateRangeForEntireFile(); m_context->PushPreparedRequest(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { args.m_range = FileRange::CreateRangeForEntireFile(); m_context->PushPreparedRequest(request); @@ -125,28 +125,28 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { ReadFile(request, args); return; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { CreateDedicatedCache(request, args); return; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { DestroyDedicatedCache(request, args); return; } else { - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { FlushCache(args.m_path); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushEntireCache(); } @@ -200,7 +200,7 @@ namespace AZ::IO } } - void DedicatedCache::ReadFile(FileRequest* request, FileRequest::ReadData& data) + void DedicatedCache::ReadFile(FileRequest* request, Requests::ReadData& data) { size_t index = FindCache(data.m_path, data.m_offset); if (index == s_fileNotFound) @@ -255,7 +255,7 @@ namespace AZ::IO StreamStackEntry::CollectStatistics(statistics); } - void DedicatedCache::CreateDedicatedCache(FileRequest* request, FileRequest::CreateDedicatedCacheData& data) + void DedicatedCache::CreateDedicatedCache(FileRequest* request, Requests::CreateDedicatedCacheData& data) { size_t index = FindCache(data.m_path, data.m_range); if (index == s_fileNotFound) @@ -276,7 +276,7 @@ namespace AZ::IO m_context->MarkRequestAsCompleted(request); } - void DedicatedCache::DestroyDedicatedCache(FileRequest* request, FileRequest::DestroyDedicatedCacheData& data) + void DedicatedCache::DestroyDedicatedCache(FileRequest* request, Requests::DestroyDedicatedCacheData& data) { size_t index = FindCache(data.m_path, data.m_range); if (index != s_fileNotFound) diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.h b/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.h index 0ef2d879d3..a69dcdbd7f 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/DedicatedCache.h @@ -11,15 +11,21 @@ #include #include #include -#include #include +#include #include -#include #include +#include #include namespace AZ::IO { + namespace Requests + { + struct CreateDedicatedCacheData; + struct DestroyDedicatedCacheData; + } // namespace Requests + struct DedicatedCacheConfig final : public IStreamerStackConfig { @@ -56,16 +62,19 @@ namespace AZ::IO void UpdateStatus(Status& status) const override; - void UpdateCompletionEstimates(AZStd::chrono::system_clock::time_point now, AZStd::vector& internalPending, - StreamerContext::PreparedQueue::iterator pendingBegin, StreamerContext::PreparedQueue::iterator pendingEnd) override; + void UpdateCompletionEstimates( + AZStd::chrono::system_clock::time_point now, + AZStd::vector& internalPending, + StreamerContext::PreparedQueue::iterator pendingBegin, + StreamerContext::PreparedQueue::iterator pendingEnd) override; void CollectStatistics(AZStd::vector& statistics) const override; private: - void CreateDedicatedCache(FileRequest* request, FileRequest::CreateDedicatedCacheData& data); - void DestroyDedicatedCache(FileRequest* request, FileRequest::DestroyDedicatedCacheData& data); + void CreateDedicatedCache(FileRequest* request, Requests::CreateDedicatedCacheData& data); + void DestroyDedicatedCache(FileRequest* request, Requests::DestroyDedicatedCacheData& data); - void ReadFile(FileRequest* request, FileRequest::ReadData& data); + void ReadFile(FileRequest* request, AZ::IO::Requests::ReadData& data); size_t FindCache(const RequestPath& filename, FileRange range); size_t FindCache(const RequestPath& filename, u64 offset); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp index fc05b77b36..2f96d224e6 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp @@ -12,22 +12,30 @@ #include #include -namespace AZ::IO +// +// Command structures. +// + +namespace AZ::IO::Requests { - // - // Command structures. - // + ReadData::ReadData(void* output, u64 outputSize, const RequestPath& path, u64 offset, u64 size, bool sharedRead) + : m_path(path) + , m_output(output) + , m_outputSize(outputSize) + , m_offset(offset) + , m_size(size) + , m_sharedRead(sharedRead) + { + } - FileRequest::ExternalRequestData::ExternalRequestData(FileRequestPtr&& request) - : m_request(AZStd::move(request)) - {} - - FileRequest::RequestPathStoreData::RequestPathStoreData(RequestPath path) - : m_path(AZStd::move(path)) - {} - - FileRequest::ReadRequestData::ReadRequestData(RequestPath path, void* output, u64 outputSize, u64 offset, u64 size, - AZStd::chrono::system_clock::time_point deadline, IStreamerTypes::Priority priority) + ReadRequestData::ReadRequestData( + RequestPath path, + void* output, + u64 outputSize, + u64 offset, + u64 size, + AZStd::chrono::system_clock::time_point deadline, + IStreamerTypes::Priority priority) : m_path(AZStd::move(path)) , m_allocator(nullptr) , m_deadline(deadline) @@ -37,10 +45,16 @@ namespace AZ::IO , m_size(size) , m_priority(priority) , m_memoryType(IStreamerTypes::MemoryType::ReadWrite) // Only generic memory can be assigned externally. - {} + { + } - FileRequest::ReadRequestData::ReadRequestData(RequestPath path, IStreamerTypes::RequestMemoryAllocator* allocator, - u64 offset, u64 size, AZStd::chrono::system_clock::time_point deadline, IStreamerTypes::Priority priority) + ReadRequestData::ReadRequestData( + RequestPath path, + IStreamerTypes::RequestMemoryAllocator* allocator, + u64 offset, + u64 size, + AZStd::chrono::system_clock::time_point deadline, + IStreamerTypes::Priority priority) : m_path(AZStd::move(path)) , m_allocator(allocator) , m_deadline(deadline) @@ -50,9 +64,10 @@ namespace AZ::IO , m_size(size) , m_priority(priority) , m_memoryType(IStreamerTypes::MemoryType::ReadWrite) // Only generic memory can be assigned externally. - {} + { + } - FileRequest::ReadRequestData::~ReadRequestData() + ReadRequestData::~ReadRequestData() { if (m_allocator != nullptr) { @@ -64,65 +79,81 @@ namespace AZ::IO } } - FileRequest::ReadData::ReadData(void* output, u64 outputSize, const RequestPath& path, u64 offset, u64 size, bool sharedRead) - : m_output(output) - , m_outputSize(outputSize) - , m_path(path) - , m_offset(offset) - , m_size(size) - , m_sharedRead(sharedRead) - {} + CreateDedicatedCacheData::CreateDedicatedCacheData(RequestPath path, const FileRange& range) + : m_path(AZStd::move(path)) + , m_range(range) + { + } - FileRequest::CompressedReadData::CompressedReadData(CompressionInfo&& compressionInfo, void* output, u64 readOffset, u64 readSize) + DestroyDedicatedCacheData::DestroyDedicatedCacheData(RequestPath path, const FileRange& range) + : m_path(AZStd::move(path)) + , m_range(range) + { + } + + ExternalRequestData::ExternalRequestData(FileRequestPtr&& request) + : m_request(AZStd::move(request)) + { + } + + RequestPathStoreData::RequestPathStoreData(RequestPath path) + : m_path(AZStd::move(path)) + { + } + + CompressedReadData::CompressedReadData(CompressionInfo&& compressionInfo, void* output, u64 readOffset, u64 readSize) : m_compressionInfo(AZStd::move(compressionInfo)) , m_output(output) , m_readOffset(readOffset) , m_readSize(readSize) - {} + { + } - FileRequest::FileExistsCheckData::FileExistsCheckData(const RequestPath& path) + FileExistsCheckData::FileExistsCheckData(const RequestPath& path) : m_path(path) - {} + { + } - FileRequest::FileMetaDataRetrievalData::FileMetaDataRetrievalData(const RequestPath& path) + FileMetaDataRetrievalData::FileMetaDataRetrievalData(const RequestPath& path) : m_path(path) - {} + { + } - FileRequest::CancelData::CancelData(FileRequestPtr target) + CancelData::CancelData(FileRequestPtr target) : m_target(AZStd::move(target)) - {} + { + } - FileRequest::FlushData::FlushData(RequestPath path) + FlushData::FlushData(RequestPath path) : m_path(AZStd::move(path)) - {} + { + } - FileRequest::RescheduleData::RescheduleData(FileRequestPtr target, AZStd::chrono::system_clock::time_point newDeadline, + RescheduleData::RescheduleData( + FileRequestPtr target, + AZStd::chrono::system_clock::time_point newDeadline, IStreamerTypes::Priority newPriority) : m_target(AZStd::move(target)) , m_newDeadline(newDeadline) , m_newPriority(newPriority) - {} + { + } - FileRequest::CreateDedicatedCacheData::CreateDedicatedCacheData(RequestPath path, const FileRange& range) - : m_path(AZStd::move(path)) - , m_range(range) - {} - - FileRequest::DestroyDedicatedCacheData::DestroyDedicatedCacheData(RequestPath path, const FileRange& range) - : m_path(AZStd::move(path)) - , m_range(range) - {} - - FileRequest::ReportData::ReportData(ReportType reportType) + Requests::ReportData::ReportData(ReportType reportType) : m_reportType(reportType) - {} + { + } - FileRequest::CustomData::CustomData(AZStd::any data, bool failWhenUnhandled) + CustomData::CustomData(AZStd::any data, bool failWhenUnhandled) : m_data(AZStd::move(data)) , m_failWhenUnhandled(failWhenUnhandled) - {} - + { + } +} // namespace AZ::IO::Requests +namespace AZ::IO +{ + using namespace Requests; // // FileRequest // @@ -263,7 +294,7 @@ namespace AZ::IO SetOptionalParent(parent); } - void FileRequest::CreateReport(ReportData::ReportType reportType) + void FileRequest::CreateReport(Requests::ReportType reportType) { AZ_Assert(AZStd::holds_alternative(m_command), "Attempting to set FileRequest to 'Report', but another task was already assigned."); @@ -361,7 +392,7 @@ namespace AZ::IO "Request does not contain a valid command. It may have been reset already or was never assigned a command."); return true; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { return args.m_failWhenUnhandled; } diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.h b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.h index dd4dad2387..7f5874c95d 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.h @@ -27,7 +27,252 @@ namespace AZ::IO class ExternalFileRequest; using FileRequestPtr = AZStd::intrusive_ptr; +} // namespace AZ::IO +namespace AZ::IO::Requests +{ + //! Request to read data. This is a translated request and holds an absolute path and has been + //! resolved to the archive file if needed. + struct ReadData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + + ReadData(void* output, u64 outputSize, const RequestPath& path, u64 offset, u64 size, bool sharedRead); + + const RequestPath& m_path; //!< The path to the file that contains the requested data. + void* m_output; //!< Target output to write the read data to. + u64 m_outputSize; //!< Size of memory m_output points to. This needs to be at least as big as m_size, but can be bigger. + u64 m_offset; //!< The offset in bytes into the file. + u64 m_size; //!< The number of bytes to read from the file. + bool m_sharedRead; //!< True if other code will be reading from the file or the stack entry can exclusively lock. + }; + + //! Request to read data. This is an untranslated request and holds a relative path. The Scheduler + //! will translate this to the appropriate ReadData or CompressedReadData. + struct ReadRequestData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + + ReadRequestData( + RequestPath path, + void* output, + u64 outputSize, + u64 offset, + u64 size, + AZStd::chrono::system_clock::time_point deadline, + IStreamerTypes::Priority priority); + ReadRequestData( + RequestPath path, + IStreamerTypes::RequestMemoryAllocator* allocator, + u64 offset, + u64 size, + AZStd::chrono::system_clock::time_point deadline, + IStreamerTypes::Priority priority); + ~ReadRequestData(); + + RequestPath m_path; //!< Relative path to the target file. + IStreamerTypes::RequestMemoryAllocator* m_allocator; //!< Allocator used to manage the memory for this request. + AZStd::chrono::system_clock::time_point m_deadline; //!< Time by which this request should have been completed. + void* m_output; //!< The memory address assigned (during processing) to store the read data to. + u64 m_outputSize; //!< The memory size of the addressed used to store the read data. + u64 m_offset; //!< The offset in bytes into the file. + u64 m_size; //!< The number of bytes to read from the file. + IStreamerTypes::Priority m_priority; //!< Priority used for ordering requests. This is used when requests have the same deadline. + IStreamerTypes::MemoryType m_memoryType; //!< The type of memory provided by the allocator if used. + }; + + //! Creates a cache dedicated to a single file. This is best used for files where blocks are read from + //! periodically such as audio banks of video files. + struct CreateDedicatedCacheData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + CreateDedicatedCacheData(RequestPath path, const FileRange& range); + + RequestPath m_path; + FileRange m_range; + }; + + //! Destroys a cache dedicated to a single file that was previously created by CreateDedicatedCache + struct DestroyDedicatedCacheData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + DestroyDedicatedCacheData(RequestPath path, const FileRange& range); + + RequestPath m_path; + FileRange m_range; + }; + + enum class ReportType : int8_t + { + FileLocks + }; + + struct ReportData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityLow; + inline constexpr static bool s_failWhenUnhandled = false; + + explicit ReportData(ReportType reportType); + + ReportType m_reportType; + }; + + //! Stores a reference to the external request so it stays alive while the request is being processed. + //! This is needed because Streamer supports fire-and-forget requests since completion can be handled by + //! registering a callback. + struct ExternalRequestData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + + explicit ExternalRequestData(FileRequestPtr&& request); + + FileRequestPtr m_request; //!< The request that was send to Streamer. + }; + + //! Stores an instance of a RequestPath. To reduce copying instances of a RequestPath functions that + //! need a path take them by reference to the original request. In some cases a path originates from + //! within in the stack and temporary storage is needed. This struct allows for that temporary storage + //! so it can be safely referenced later. + struct RequestPathStoreData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + + explicit RequestPathStoreData(RequestPath path); + + RequestPath m_path; + }; + + //! Request to read and decompress data. + struct CompressedReadData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + + CompressedReadData(CompressionInfo&& compressionInfo, void* output, u64 readOffset, u64 readSize); + + CompressionInfo m_compressionInfo; + void* m_output; //!< Target output to write the read data to. + u64 m_readOffset; //!< The offset into the decompressed to start copying from. + u64 m_readSize; //!< Number of bytes to read from the decompressed file. + }; + + //! Holds the progress of an operation chain until this request is explicitly completed. + struct WaitData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + inline constexpr static bool s_failWhenUnhandled = true; + }; + + //! Checks to see if any node in the stack can find a file at the provided path. + struct FileExistsCheckData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + explicit FileExistsCheckData(const RequestPath& path); + + const RequestPath& m_path; + bool m_found{ false }; + }; + + //! Searches for a file in the stack and retrieves the meta data. This may be slower than a file exists + //! check. + struct FileMetaDataRetrievalData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + explicit FileMetaDataRetrievalData(const RequestPath& path); + + const RequestPath& m_path; + u64 m_fileSize{ 0 }; + bool m_found{ false }; + }; + + //! Cancels a request in the stream stack, if possible. + struct CancelData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHighest; + inline constexpr static bool s_failWhenUnhandled = false; + + explicit CancelData(FileRequestPtr target); + + FileRequestPtr m_target; //!< The request that will be canceled. + }; + + //! Updates the priority and deadline of a request that has not been queued yet. + struct RescheduleData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + RescheduleData(FileRequestPtr target, AZStd::chrono::system_clock::time_point newDeadline, IStreamerTypes::Priority newPriority); + + FileRequestPtr m_target; //!< The request that will be rescheduled. + AZStd::chrono::system_clock::time_point m_newDeadline; //!< The new deadline for the request. + IStreamerTypes::Priority m_newPriority; //!< The new priority for the request. + }; + + //! Flushes all references to the provided file in the streaming stack. + struct FlushData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + + explicit FlushData(RequestPath path); + + RequestPath m_path; + }; + + //! Flushes all caches in the streaming stack. + struct FlushAllData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; + inline constexpr static bool s_failWhenUnhandled = false; + }; + + //! Data for a custom command. This can be used by nodes added extensions that need data that can't be stored + //! in the already provided data. + struct CustomData + { + inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; + + CustomData(AZStd::any data, bool failWhenUnhandled); + + AZStd::any m_data; //!< The data for the custom request. + bool m_failWhenUnhandled; //!< Whether or not the request is marked as failed or success when no node process it. + }; + using CommandVariant = AZStd::variant< + AZStd::monostate, + ExternalRequestData, + RequestPathStoreData, + ReadRequestData, + ReadData, + CompressedReadData, + WaitData, + FileExistsCheckData, + FileMetaDataRetrievalData, + CancelData, + RescheduleData, + FlushData, + FlushAllData, + CreateDedicatedCacheData, + DestroyDedicatedCacheData, + ReportData, + CustomData>; + +} // namespace AZ::IO::Requests + +namespace AZ::IO +{ class FileRequest final { public: @@ -36,218 +281,7 @@ namespace AZ::IO friend class StreamerContext; friend class ExternalFileRequest; - //! Stores a reference to the external request so it stays alive while the request is being processed. - //! This is needed because Streamer supports fire-and-forget requests since completion can be handled by - //! registering a callback. - struct ExternalRequestData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - - explicit ExternalRequestData(FileRequestPtr&& request); - - FileRequestPtr m_request; //!< The request that was send to Streamer. - }; - - //! Stores an instance of a RequestPath. To reduce copying instances of a RequestPath functions that - //! need a path take them by reference to the original request. In some cases a path originates from - //! within in the stack and temporary storage is needed. This struct allows for that temporary storage - //! so it can be safely referenced later. - struct RequestPathStoreData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - - explicit RequestPathStoreData(RequestPath path); - - RequestPath m_path; - }; - - //! Request to read data. This is an untranslated request and holds a relative path. The Scheduler - //! will translate this to the appropriate ReadData or CompressedReadData. - struct ReadRequestData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - - ReadRequestData(RequestPath path, void* output, u64 outputSize, u64 offset, u64 size, - AZStd::chrono::system_clock::time_point deadline, IStreamerTypes::Priority priority); - ReadRequestData(RequestPath path, IStreamerTypes::RequestMemoryAllocator* allocator, u64 offset, u64 size, - AZStd::chrono::system_clock::time_point deadline, IStreamerTypes::Priority priority); - ~ReadRequestData(); - - RequestPath m_path; //!< Relative path to the target file. - IStreamerTypes::RequestMemoryAllocator* m_allocator; //!< Allocator used to manage the memory for this request. - AZStd::chrono::system_clock::time_point m_deadline; //!< Time by which this request should have been completed. - void* m_output; //!< The memory address assigned (during processing) to store the read data to. - u64 m_outputSize; //!< The memory size of the addressed used to store the read data. - u64 m_offset; //!< The offset in bytes into the file. - u64 m_size; //!< The number of bytes to read from the file. - IStreamerTypes::Priority m_priority; //!< Priority used for ordering requests. This is used when requests have the same deadline. - IStreamerTypes::MemoryType m_memoryType; //!< The type of memory provided by the allocator if used. - }; - - //! Request to read data. This is a translated request and holds an absolute path and has been - //! resolved to the archive file if needed. - struct ReadData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - - ReadData(void* output, u64 outputSize, const RequestPath& path, u64 offset, u64 size, bool sharedRead); - - const RequestPath& m_path; //!< The path to the file that contains the requested data. - void* m_output; //!< Target output to write the read data to. - u64 m_outputSize; //!< Size of memory m_output points to. This needs to be at least as big as m_size, but can be bigger. - u64 m_offset; //!< The offset in bytes into the file. - u64 m_size; //!< The number of bytes to read from the file. - bool m_sharedRead; //!< True if other code will be reading from the file or the stack entry can exclusively lock. - }; - - //! Request to read and decompress data. - struct CompressedReadData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - - CompressedReadData(CompressionInfo&& compressionInfo, void* output, u64 readOffset, u64 readSize); - - CompressionInfo m_compressionInfo; - void* m_output; //!< Target output to write the read data to. - u64 m_readOffset; //!< The offset into the decompressed to start copying from. - u64 m_readSize; //!< Number of bytes to read from the decompressed file. - }; - - //! Holds the progress of an operation chain until this request is explicitly completed. - struct WaitData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - inline constexpr static bool s_failWhenUnhandled = true; - }; - - //! Checks to see if any node in the stack can find a file at the provided path. - struct FileExistsCheckData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - explicit FileExistsCheckData(const RequestPath& path); - - const RequestPath& m_path; - bool m_found{ false }; - }; - - //! Searches for a file in the stack and retrieves the meta data. This may be slower than a file exists - //! check. - struct FileMetaDataRetrievalData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - explicit FileMetaDataRetrievalData(const RequestPath& path); - - const RequestPath& m_path; - u64 m_fileSize{ 0 }; - bool m_found{ false }; - }; - - //! Cancels a request in the stream stack, if possible. - struct CancelData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHighest; - inline constexpr static bool s_failWhenUnhandled = false; - - explicit CancelData(FileRequestPtr target); - - FileRequestPtr m_target; //!< The request that will be canceled. - }; - - //! Updates the priority and deadline of a request that has not been queued yet. - struct RescheduleData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - RescheduleData(FileRequestPtr target, AZStd::chrono::system_clock::time_point newDeadline, IStreamerTypes::Priority newPriority); - - FileRequestPtr m_target; //!< The request that will be rescheduled. - AZStd::chrono::system_clock::time_point m_newDeadline; //!< The new deadline for the request. - IStreamerTypes::Priority m_newPriority; //!< The new priority for the request. - }; - - //! Flushes all references to the provided file in the streaming stack. - struct FlushData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - explicit FlushData(RequestPath path); - - RequestPath m_path; - }; - - //! Flushes all caches in the streaming stack. - struct FlushAllData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - }; - - //! Creates a cache dedicated to a single file. This is best used for files where blocks are read from - //! periodically such as audio banks of video files. - struct CreateDedicatedCacheData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - CreateDedicatedCacheData(RequestPath path, const FileRange& range); - - RequestPath m_path; - FileRange m_range; - }; - - //! Destroys a cache dedicated to a single file that was previously created by CreateDedicatedCache - struct DestroyDedicatedCacheData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityHigh; - inline constexpr static bool s_failWhenUnhandled = false; - - DestroyDedicatedCacheData(RequestPath path, const FileRange& range); - - RequestPath m_path; - FileRange m_range; - }; - - struct ReportData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityLow; - inline constexpr static bool s_failWhenUnhandled = false; - - enum class ReportType - { - FileLocks - }; - - explicit ReportData(ReportType reportType); - - ReportType m_reportType; - }; - - //! Data for a custom command. This can be used by nodes added extensions that need data that can't be stored - //! in the already provided data. - struct CustomData - { - inline constexpr static IStreamerTypes::Priority s_orderPriority = IStreamerTypes::s_priorityMedium; - - CustomData(AZStd::any data, bool failWhenUnhandled); - - AZStd::any m_data; //!< The data for the custom request. - bool m_failWhenUnhandled; //!< Whether or not the request is marked as failed or success when no node process it. - }; - - using CommandVariant = AZStd::variant; + using CommandVariant = Requests::CommandVariant; using OnCompletionCallback = AZStd::function; AZ_CLASS_ALLOCATOR(FileRequest, SystemAllocator, 0); @@ -278,7 +312,7 @@ namespace AZ::IO void CreateFlushAll(); void CreateDedicatedCacheCreation(RequestPath path, const FileRange& range = {}, FileRequest* parent = nullptr); void CreateDedicatedCacheDestruction(RequestPath path, const FileRange& range = {}, FileRequest* parent = nullptr); - void CreateReport(ReportData::ReportType reportType); + void CreateReport(Requests::ReportType reportType); void CreateCustom(AZStd::any data, bool failWhenUnhandled = true, FileRequest* parent = nullptr); void SetCompletionCallback(OnCompletionCallback callback); @@ -325,8 +359,17 @@ namespace AZ::IO //! Command and parameters for the request. CommandVariant m_command; - //! Status of the request. - AZStd::atomic m_status{ IStreamerTypes::RequestStatus::Pending }; + //! Estimated time this request will complete. This is an estimation and depends on many + //! factors which can cause it to change drastically from moment to moment. + AZStd::chrono::system_clock::time_point m_estimatedCompletion; + + //! The file request that has a dependency on this one. This can be null if there are no + //! other request depending on this one to complete. + FileRequest* m_parent{ nullptr }; + + + //! Id assigned when the request is added to the pending queue. + size_t m_pendingId{ 0 }; //! Called once the request has completed. This will always be called from the Streamer thread //! and thread safety is the responsibility of called function. When assigning a lambda avoid @@ -336,16 +379,8 @@ namespace AZ::IO //! a longer running task is needed consider using a job to do the work. OnCompletionCallback m_onCompletion; - //! Estimated time this request will complete. This is an estimation and depends on many - //! factors which can cause it to change drastically from moment to moment. - AZStd::chrono::system_clock::time_point m_estimatedCompletion; - - //! The file request that has a dependency on this one. This can be null if there are no - //! other request depending on this one to complete. - FileRequest* m_parent{ nullptr }; - - //! Id assigned when the request is added to the pending queue. - size_t m_pendingId{ 0 }; + //! Status of the request. + AZStd::atomic m_status{ IStreamerTypes::RequestStatus::Pending }; //! The number of dependent file request that need to complete before this one is done. u16 m_dependencies{ 0 }; diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.cpp index 723a5d62c8..ac9344dd28 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.cpp @@ -91,12 +91,12 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { PrepareReadRequest(request, args); } - else if constexpr (AZStd::is_same_v || - AZStd::is_same_v) + else if constexpr (AZStd::is_same_v || + AZStd::is_same_v) { PrepareDedicatedCache(request, args.m_path); } @@ -114,11 +114,11 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { m_pendingReads.push_back(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { m_pendingFileExistChecks.push_back(request); } @@ -203,7 +203,7 @@ namespace AZ::IO { FileRequest* compressedRequest = m_processingJobs[i].m_waitRequest->GetParent(); AZ_Assert(compressedRequest, "A wait request attached to FullFileDecompressor was completed but didn't have a parent compressed request."); - auto data = AZStd::get_if(&compressedRequest->GetCommand()); + auto data = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(data, "Compressed request in the decompression queue in FullFileDecompressor didn't contain compression read data."); size_t bytesToDecompress = data->m_compressionInfo.m_compressedSize; @@ -255,7 +255,7 @@ namespace AZ::IO // Calculate the amount of time it will take to decompress the data. FileRequest* compressedRequest = m_readRequests[i]->GetParent(); - auto data = AZStd::get_if(&compressedRequest->GetCommand()); + auto data = AZStd::get_if(&compressedRequest->GetCommand()); size_t bytesToDecompress = data->m_compressionInfo.m_compressedSize; auto decompressionDuration = AZStd::chrono::microseconds( @@ -290,7 +290,7 @@ namespace AZ::IO void FullFileDecompressor::EstimateCompressedReadRequest(FileRequest* request, AZStd::chrono::microseconds& cumulativeDelay, AZStd::chrono::microseconds decompressionDelay, double totalDecompressionDurationUs, double totalBytesDecompressed) const { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); if (data) { AZStd::chrono::microseconds processingTime = decompressionDelay; @@ -343,7 +343,7 @@ namespace AZ::IO m_numRunningJobs == 0; } - void FullFileDecompressor::PrepareReadRequest(FileRequest* request, FileRequest::ReadRequestData& data) + void FullFileDecompressor::PrepareReadRequest(FileRequest* request, Requests::ReadRequestData &data) { CompressionInfo info; if (CompressionUtils::FindCompressionInfo(info, data.m_path.GetRelativePath())) @@ -359,7 +359,7 @@ namespace AZ::IO { FileRequest* pathStorageRequest = m_context->GetNewInternalRequest(); pathStorageRequest->CreateRequestPathStore(request, AZStd::move(info.m_archiveFilename)); - auto& pathStorage = AZStd::get(pathStorageRequest->GetCommand()); + auto& pathStorage = AZStd::get(pathStorageRequest->GetCommand()); nextRequest->CreateRead(pathStorageRequest, data.m_output, data.m_outputSize, pathStorage.m_path, info.m_offset + data.m_offset, data.m_size, info.m_isSharedPak); @@ -370,13 +370,13 @@ namespace AZ::IO auto callback = [this, nextRequest](const FileRequest& checkRequest) { AZ_PROFILE_FUNCTION(AzCore); - auto check = AZStd::get_if(&checkRequest.GetCommand()); + auto check = AZStd::get_if(&checkRequest.GetCommand()); AZ_Assert(check, "Callback in FullFileDecompressor::PrepareReadRequest expected FileExistsCheck but got another command."); if (check->m_found) { FileRequest* originalRequest = m_context->RejectRequest(nextRequest); - if (AZStd::holds_alternative(originalRequest->GetCommand())) + if (AZStd::holds_alternative(originalRequest->GetCommand())) { originalRequest = m_context->RejectRequest(originalRequest); } @@ -412,12 +412,12 @@ namespace AZ::IO AZStd::visit([request, &info, nextRequest](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { nextRequest->CreateDedicatedCacheCreation(AZStd::move(info.m_archiveFilename), FileRange::CreateRange(info.m_offset, info.m_compressedSize), request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { nextRequest->CreateDedicatedCacheDestruction(AZStd::move(info.m_archiveFilename), FileRange::CreateRange(info.m_offset, info.m_compressedSize), request); @@ -429,7 +429,7 @@ namespace AZ::IO auto callback = [this, nextRequest](const FileRequest& checkRequest) { AZ_PROFILE_FUNCTION(AzCore); - auto check = AZStd::get_if(&checkRequest.GetCommand()); + auto check = AZStd::get_if(&checkRequest.GetCommand()); AZ_Assert(check, "Callback in FullFileDecompressor::PrepareDedicatedCache expected FileExistsCheck but got another command."); if (check->m_found) @@ -461,7 +461,7 @@ namespace AZ::IO void FullFileDecompressor::FileExistsCheck(FileRequest* checkRequest) { - auto& fileCheckRequest = AZStd::get(checkRequest->GetCommand()); + auto& fileCheckRequest = AZStd::get(checkRequest->GetCommand()); CompressionInfo info; if (CompressionUtils::FindCompressionInfo(info, fileCheckRequest.m_path.GetRelativePath())) { @@ -487,7 +487,7 @@ namespace AZ::IO { if (m_readBufferStatus[i] == ReadBufferStatus::Unused) { - auto data = AZStd::get_if(&compressedReadRequest->GetCommand()); + auto data = AZStd::get_if(&compressedReadRequest->GetCommand()); AZ_Assert(data, "Compressed request that's starting a read in FullFileDecompressor didn't contain compression read data."); AZ_Assert(data->m_compressionInfo.m_decompressor, "FileRequest for FullFileDecompressor is missing a decompression callback."); @@ -549,7 +549,7 @@ namespace AZ::IO } else { - auto data = AZStd::get_if(&compressedRequest->GetCommand()); + auto data = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(data, "Compressed request in FullFileDecompressor that finished unsuccessfully didn't contain compression read data."); CompressionInfo& info = data->m_compressionInfo; size_t offsetAdjustment = info.m_offset - AZ_SIZE_ALIGN_DOWN(info.m_offset, aznumeric_cast(m_alignment)); @@ -591,7 +591,7 @@ namespace AZ::IO } FileRequest* waitRequest = m_readRequests[readSlot]; - AZ_Assert(AZStd::holds_alternative(waitRequest->GetCommand()), + AZ_Assert(AZStd::holds_alternative(waitRequest->GetCommand()), "File request waiting for decompression wasn't marked as being a wait operation."); FileRequest* compressedRequest = waitRequest->GetParent(); AZ_Assert(compressedRequest, "Read requests started by FullFileDecompressor is missing a parent request."); @@ -610,7 +610,7 @@ namespace AZ::IO m_readBuffers[readSlot] = nullptr; AZ::Job* decompressionJob; - auto data = AZStd::get_if(&compressedRequest->GetCommand()); + auto data = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(data, "Compressed request in FullFileDecompressor that's starting decompression didn't contain compression read data."); AZ_Assert(data->m_compressionInfo.m_decompressor, "FullFileDecompressor is queuing a decompression job but couldn't find a decompressor."); @@ -664,7 +664,7 @@ namespace AZ::IO FileRequest* compressedRequest = jobInfo.m_waitRequest->GetParent(); AZ_Assert(compressedRequest, "A wait request attached to FullFileDecompressor was completed but didn't have a parent compressed request."); - auto data = AZStd::get_if(&compressedRequest->GetCommand()); + auto data = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(data, "Compressed request in FullFileDecompressor that completed decompression didn't contain compression read data."); CompressionInfo& info = data->m_compressionInfo; size_t offsetAdjustment = info.m_offset - AZ_SIZE_ALIGN_DOWN(info.m_offset, aznumeric_cast(m_alignment)); @@ -694,7 +694,7 @@ namespace AZ::IO FileRequest* compressedRequest = info.m_waitRequest->GetParent(); AZ_Assert(compressedRequest, "A wait request attached to FullFileDecompressor was completed but didn't have a parent compressed request."); - auto request = AZStd::get_if(&compressedRequest->GetCommand()); + auto request = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(request, "Compressed request in FullFileDecompressor that's running full decompression didn't contain compression read data."); CompressionInfo& compressionInfo = request->m_compressionInfo; AZ_Assert(compressionInfo.m_decompressor, "Full decompressor job started, but there's no decompressor callback assigned."); @@ -719,7 +719,7 @@ namespace AZ::IO FileRequest* compressedRequest = info.m_waitRequest->GetParent(); AZ_Assert(compressedRequest, "A wait request attached to FullFileDecompressor was completed but didn't have a parent compressed request."); - auto request = AZStd::get_if(&compressedRequest->GetCommand()); + auto request = AZStd::get_if(&compressedRequest->GetCommand()); AZ_Assert(request, "Compressed request in FullFileDecompressor that's running partial decompression didn't contain compression read data."); CompressionInfo& compressionInfo = request->m_compressionInfo; AZ_Assert(compressionInfo.m_decompressor, "Partial decompressor job started, but there's no decompressor callback assigned."); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.h b/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.h index d9bd68f1a1..cb4226fd7c 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/FullFileDecompressor.h @@ -21,6 +21,11 @@ namespace AZ::IO { + namespace Requests + { + struct ReadRequestData; + } + struct FullFileDecompressorConfig final : public IStreamerStackConfig { @@ -87,7 +92,7 @@ namespace AZ::IO bool IsIdle() const; - void PrepareReadRequest(FileRequest* request, FileRequest::ReadRequestData& data); + void PrepareReadRequest(FileRequest* request, Requests::ReadRequestData& data); void PrepareDedicatedCache(FileRequest* request, const RequestPath& path); void FileExistsCheck(FileRequest* checkRequest); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/ReadSplitter.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/ReadSplitter.cpp index a952e31a93..282a1e15c9 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/ReadSplitter.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/ReadSplitter.cpp @@ -118,7 +118,7 @@ namespace AZ::IO return; } - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); if (data == nullptr) { StreamStackEntry::QueueRequest(request); @@ -156,7 +156,7 @@ namespace AZ::IO void ReadSplitter::QueueAlignedRead(FileRequest* request) { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); AZ_Assert(data != nullptr, "Provided request to queue by the Read Splitter did not contain a read command."); if (data->m_size <= m_maxReadSize) @@ -187,7 +187,7 @@ namespace AZ::IO bool ReadSplitter::QueueAlignedRead(PendingRead& pending) { - auto data = AZStd::get_if(&pending.m_request->GetCommand()); + auto data = AZStd::get_if(&pending.m_request->GetCommand()); AZ_Assert(data != nullptr, "Provided request to queue by the Read Splitter did not contain a read command."); while (pending.m_readSize > 0) @@ -237,7 +237,7 @@ namespace AZ::IO void ReadSplitter::QueueBufferedRead(FileRequest* request) { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); AZ_Assert(data != nullptr, "Provided request to queue by the Read Splitter did not contain a read command."); PendingRead pendingRead; @@ -262,7 +262,7 @@ namespace AZ::IO bool ReadSplitter::QueueBufferedRead(PendingRead& pending) { - auto data = AZStd::get_if(&pending.m_request->GetCommand()); + auto data = AZStd::get_if(&pending.m_request->GetCommand()); AZ_Assert(data != nullptr, "Provided request to queue by the Read Splitter did not contain a read command."); while (pending.m_readSize > 0) diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.cpp index fe1d5a5eda..65f72946b0 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.cpp @@ -6,9 +6,11 @@ * */ +#include + +#include #include #include -#include #include #include @@ -35,6 +37,10 @@ namespace AZ::IO m_threadData.m_streamStack = AZStd::move(streamStack); } + Scheduler::~Scheduler() + { + } + void Scheduler::Start(const AZStd::thread_desc& threadDesc) { if (!m_isRunning) @@ -222,10 +228,10 @@ namespace AZ::IO { using Command = AZStd::decay_t; if constexpr ( - AZStd::is_same_v || - AZStd::is_same_v) + AZStd::is_same_v || + AZStd::is_same_v) { - auto parentReadRequest = next->GetCommandFromChain(); + auto parentReadRequest = next->GetCommandFromChain(); AZ_Assert(parentReadRequest != nullptr, "The issued read request can't be found for the (compressed) read command."); size_t size = parentReadRequest->m_size; @@ -234,7 +240,7 @@ namespace AZ::IO AZ_Assert(parentReadRequest->m_allocator, "The read request was issued without a memory allocator or valid output address."); u64 recommendedSize = size; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { recommendedSize = m_recommendations.CalculateRecommendedMemorySize(size, parentReadRequest->m_offset); } @@ -249,12 +255,12 @@ namespace AZ::IO parentReadRequest->m_output = allocation.m_address; parentReadRequest->m_outputSize = allocation.m_size; parentReadRequest->m_memoryType = allocation.m_type; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { args.m_output = parentReadRequest->m_output; args.m_outputSize = allocation.m_size; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { args.m_output = parentReadRequest->m_output; } @@ -267,7 +273,7 @@ namespace AZ::IO } #endif - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { m_threadData.m_lastFilePath = args.m_path; m_threadData.m_lastFileOffset = args.m_offset + args.m_size; @@ -275,7 +281,7 @@ namespace AZ::IO m_processingSize += args.m_size; #endif } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { const CompressionInfo& info = args.m_compressionInfo; m_threadData.m_lastFilePath = info.m_archiveFilename; @@ -288,15 +294,15 @@ namespace AZ::IO "Streamer queued %zu: %s", next->GetCommand().index(), parentReadRequest->m_path.GetRelativePath()); m_threadData.m_streamStack->QueueRequest(next); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { return Thread_ProcessCancelRequest(next, args); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { return Thread_ProcessRescheduleRequest(next, args); } - else if constexpr (AZStd::is_same_v || AZStd::is_same_v) + else if constexpr (AZStd::is_same_v || AZStd::is_same_v) { AZ_PROFILE_INTERVAL_START_COLORED(AzCore, next, ProfilerColor, "Streamer queued %zu", next->GetCommand().index()); @@ -345,7 +351,7 @@ namespace AZ::IO #endif { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { if (args.m_output == nullptr && args.m_allocator != nullptr) { @@ -393,7 +399,7 @@ namespace AZ::IO } } - void Scheduler::Thread_ProcessCancelRequest(FileRequest* request, FileRequest::CancelData& data) + void Scheduler::Thread_ProcessCancelRequest(FileRequest* request, Requests::CancelData& data) { AZ_PROFILE_INTERVAL_START_COLORED(AzCore, request, ProfilerColor, "Streamer queued cancel"); auto& pending = m_context.GetPreparedRequests(); @@ -415,7 +421,7 @@ namespace AZ::IO m_threadData.m_streamStack->QueueRequest(request); } - void Scheduler::Thread_ProcessRescheduleRequest(FileRequest* request, FileRequest::RescheduleData& data) + void Scheduler::Thread_ProcessRescheduleRequest(FileRequest* request, Requests::RescheduleData& data) { AZ_PROFILE_INTERVAL_START_COLORED(AzCore, request, ProfilerColor, "Streamer queued reschedule"); auto& pendingRequests = m_context.GetPreparedRequests(); @@ -424,7 +430,7 @@ namespace AZ::IO if (pending->WorksOn(data.m_target)) { // Read requests are the only requests that use deadlines and dynamic priorities. - auto readRequest = pending->GetCommandFromChain(); + auto readRequest = pending->GetCommandFromChain(); if (readRequest) { readRequest->m_deadline = data.m_newDeadline; @@ -463,8 +469,8 @@ namespace AZ::IO // Order is the same for both requests, so prioritize the request that are at risk of missing // it's deadline. - const FileRequest::ReadRequestData* firstRead = first->GetCommandFromChain(); - const FileRequest::ReadRequestData* secondRead = second->GetCommandFromChain(); + const Requests::ReadRequestData* firstRead = first->GetCommandFromChain(); + const Requests::ReadRequestData* secondRead = second->GetCommandFromChain(); if (firstRead == nullptr || secondRead == nullptr) { @@ -496,11 +502,11 @@ namespace AZ::IO auto sameFile = [this](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { return m_threadData.m_lastFilePath == args.m_path; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { return m_threadData.m_lastFilePath == args.m_compressionInfo.m_archiveFilename; } @@ -517,11 +523,11 @@ namespace AZ::IO auto offset = [](auto&& args) -> s64 { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { return aznumeric_caster(args.m_offset); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { return aznumeric_caster(args.m_compressionInfo.m_offset); } diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.h b/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.h index 053a57d332..502002fd27 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/Scheduler.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -24,11 +25,19 @@ namespace AZ::IO { class FileRequest; + namespace Requests + { + struct CancelData; + struct RescheduleData; + } // namespace Requests + class Scheduler final { public: explicit Scheduler(AZStd::shared_ptr streamStack, u64 memoryAlignment = AZCORE_GLOBAL_NEW_ALIGNMENT, u64 sizeAlignment = 1, u64 granularity = 1_mib); + ~Scheduler(); + void Start(const AZStd::thread_desc& threadDesc); void Stop(); @@ -61,14 +70,14 @@ namespace AZ::IO bool Thread_ExecuteRequests(); bool Thread_PrepareRequests(AZStd::vector& outstandingRequests); void Thread_ProcessTillIdle(); - void Thread_ProcessCancelRequest(FileRequest* request, FileRequest::CancelData& data); - void Thread_ProcessRescheduleRequest(FileRequest* request, FileRequest::RescheduleData& data); + void Thread_ProcessCancelRequest(FileRequest* request, Requests::CancelData& data); + void Thread_ProcessRescheduleRequest(FileRequest* request, Requests::RescheduleData& data); enum class Order { - FirstRequest, //< The first request is the most important to process next. - SecondRequest, //< The second request is the most important to process next. - Equal //< Both requests are equally important. + FirstRequest, //!< The first request is the most important to process next. + SecondRequest, //!< The second request is the most important to process next. + Equal //!< Both requests are equally important. }; //! Determine which of the two provided requests is more important to process next. Order Thread_PrioritizeRequests(const FileRequest* first, const FileRequest* second) const; diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.cpp index cfd191b68f..701eb563ec 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.cpp @@ -60,9 +60,9 @@ namespace AZ::IO AZ_PROFILE_FUNCTION(AzCore); AZ_Assert(request, "PrepareRequest was provided a null request."); - if (AZStd::holds_alternative(request->GetCommand())) + if (AZStd::holds_alternative(request->GetCommand())) { - auto& readRequest = AZStd::get(request->GetCommand()); + auto& readRequest = AZStd::get(request->GetCommand()); FileRequest* read = m_context->GetNewInternalRequest(); read->CreateRead(request, readRequest.m_output, readRequest.m_outputSize, readRequest.m_path, @@ -79,29 +79,29 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v || - AZStd::is_same_v || - AZStd::is_same_v) + if constexpr (AZStd::is_same_v || + AZStd::is_same_v || + AZStd::is_same_v) { m_pendingRequests.push_back(request); return; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { CancelRequest(request, args.m_target); return; } else { - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { FlushCache(args.m_path); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushEntireCache(); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { Report(args); } @@ -118,15 +118,15 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { ReadFile(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FileExistsRequest(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FileMetaDataRetrievalRequest(request); } @@ -199,25 +199,25 @@ namespace AZ::IO AZStd::visit([&](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { targetFile = &args.m_path; readSize = args.m_size; offset = args.m_offset; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { targetFile = &args.m_compressionInfo.m_archiveFilename; readSize = args.m_compressionInfo.m_compressedSize; offset = args.m_compressionInfo.m_offset; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds averageTime = m_getFileExistsTimeAverage.CalculateAverage(); startTime += averageTime; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds averageTime = m_getFileMetaDataTimeAverage.CalculateAverage(); @@ -254,7 +254,7 @@ namespace AZ::IO { AZ_PROFILE_FUNCTION(AzCore); - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); AZ_Assert(data, "FileRequest queued on StorageDrive to be read didn't contain read data."); SystemFile* file = nullptr; @@ -342,7 +342,7 @@ namespace AZ::IO AZ_PROFILE_FUNCTION(AzCore); TIMED_AVERAGE_WINDOW_SCOPE(m_getFileExistsTimeAverage); - auto& fileExists = AZStd::get(request->GetCommand()); + auto& fileExists = AZStd::get(request->GetCommand()); size_t cacheIndex = FindFileInCache(fileExists.m_path); if (cacheIndex != s_fileNotFound) { @@ -360,7 +360,7 @@ namespace AZ::IO AZ_PROFILE_FUNCTION(AzCore); TIMED_AVERAGE_WINDOW_SCOPE(m_getFileMetaDataTimeAverage); - auto& command = AZStd::get(request->GetCommand()); + auto& command = AZStd::get(request->GetCommand()); // If the file is already open, use the file handle which usually is cheaper than asking for the file by name. size_t cacheIndex = FindFileInCache(command.m_path); if (cacheIndex != s_fileNotFound) @@ -446,11 +446,11 @@ namespace AZ::IO } } - void StorageDrive::Report(const FileRequest::ReportData& data) const + void StorageDrive::Report(const Requests::ReportData& data) const { switch (data.m_reportType) { - case FileRequest::ReportData::ReportType::FileLocks: + case Requests::ReportType::FileLocks: for (u32 i = 0; i < m_fileHandles.size(); ++i) { if (m_fileHandles[i] != nullptr) diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.h b/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.h index d90b31eeec..25f3d7b353 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/StorageDrive.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -16,6 +17,11 @@ #include #include +namespace AZ::IO::Requests +{ + struct ReportData; +} + namespace AZ::IO { struct StorageDriveConfig final : @@ -72,7 +78,7 @@ namespace AZ::IO void EstimateCompletionTimeForRequest(FileRequest* request, AZStd::chrono::system_clock::time_point& startTime, const RequestPath*& activeFile, u64& activeOffset) const; - void Report(const FileRequest::ReportData& data) const; + void Report(const Requests::ReportData& data) const; TimedAverageWindow m_fileOpenCloseTimeAverage; TimedAverageWindow m_getFileExistsTimeAverage; diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.cpp index e4976f7d1c..dfc156bbee 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -210,7 +211,7 @@ namespace AZ::IO IStreamerTypes::ClaimMemory claimMemory) const { AZ_Assert(request.m_request, "The request handle provided to Streamer::GetReadRequestResult is invalid."); - auto readRequest = AZStd::get_if(&request.m_request->GetCommand()); + auto readRequest = AZStd::get_if(&request.m_request->GetCommand()); if (readRequest != nullptr) { buffer = readRequest->m_output; @@ -281,14 +282,14 @@ namespace AZ::IO } } - FileRequestPtr Streamer::Report(FileRequest::ReportData::ReportType reportType) + FileRequestPtr Streamer::Report(Requests::ReportType reportType) { FileRequestPtr result = CreateRequest(); Report(result, reportType); return result; } - FileRequestPtr& Streamer::Report(FileRequestPtr& request, FileRequest::ReportData::ReportType reportType) + FileRequestPtr& Streamer::Report(FileRequestPtr& request, Requests::ReportType reportType) { request->m_request.CreateReport(reportType); return request; diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.h b/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.h index bb363a0c64..7e3dd1a742 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/Streamer.h @@ -20,6 +20,10 @@ namespace AZStd struct thread_desc; } +namespace AZ::IO::Requests +{ + enum class ReportType : int8_t; +} namespace AZ::IO { @@ -185,9 +189,9 @@ namespace AZ::IO void RecordStatistics(); //! Tells AZ::IO::Streamer the report the information for the report to the output. - FileRequestPtr Report(FileRequest::ReportData::ReportType reportType); + FileRequestPtr Report(Requests::ReportType reportType); //! Tells AZ::IO::Streamer the report the information for the report to the output. - FileRequestPtr& Report(FileRequestPtr& request, FileRequest::ReportData::ReportType reportType); + FileRequestPtr& Report(FileRequestPtr& request, Requests::ReportType reportType); Streamer(const AZStd::thread_desc& threadDesc, AZStd::unique_ptr streamStack); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerComponent.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerComponent.cpp index 9a465021c2..25fdb9bdfb 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerComponent.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerComponent.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -207,7 +208,7 @@ namespace AZ { if (m_streamer) { - m_streamer->QueueRequest(m_streamer->Report(AZ::IO::FileRequest::ReportData::ReportType::FileLocks)); + m_streamer->QueueRequest(m_streamer->Report(AZ::IO::Requests::ReportType::FileLocks)); } } diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.cpp index 5cd483bc55..fc5d6468d8 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.cpp @@ -22,6 +22,10 @@ namespace AZ static constexpr char LatePredictionName[] = "Early completions"; static constexpr char MissedDeadlinesName[] = "Missed deadlines"; #endif // AZ_STREAMER_ADD_EXTRA_PROFILING_INFO + + StreamerContext::StreamerContext() + { + } StreamerContext::~StreamerContext() { for (FileRequest* entry : m_internalRecycleBin) @@ -204,7 +208,7 @@ namespace AZ m_latePredictionsPercentageStat.GetMostRecentSample()); } } - auto readRequest = AZStd::get_if(&top->GetCommand()); + auto readRequest = AZStd::get_if(&top->GetCommand()); if (readRequest != nullptr) { m_missedDeadlinePercentageStat.PushSample(now < readRequest->m_deadline ? 0.0 : 1.0); diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.h b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.h index 356eb7ddac..f4caaffc70 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.h +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/StreamerContext.h @@ -7,23 +7,28 @@ */ #pragma once -#include -#include #include #include #include -#include -#include -#include #include +#include +#include +#include +#include namespace AZ::IO { + class FileRequest; + class ExternalFileRequest; + + using FileRequestPtr = AZStd::intrusive_ptr; + class StreamerContext { public: using PreparedQueue = AZStd::deque; + StreamerContext(); ~StreamerContext(); //! Gets a new file request, either by creating a new instance or diff --git a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.cpp b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.cpp index feb8bce111..708bacd7cd 100644 --- a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.cpp +++ b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.cpp @@ -172,9 +172,9 @@ namespace AZ::IO AZ_PROFILE_FUNCTION(AzCore); AZ_Assert(request, "PrepareRequest was provided a null request."); - if (AZStd::holds_alternative(request->GetCommand())) + if (AZStd::holds_alternative(request->GetCommand())) { - auto& readRequest = AZStd::get(request->GetCommand()); + auto& readRequest = AZStd::get(request->GetCommand()); if (IsServicedByThisDrive(readRequest.m_path.GetAbsolutePath())) { FileRequest* read = m_context->GetNewInternalRequest(); @@ -195,7 +195,7 @@ namespace AZ::IO AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { if (IsServicedByThisDrive(args.m_path.GetAbsolutePath())) { @@ -203,8 +203,8 @@ namespace AZ::IO return; } } - else if constexpr (AZStd::is_same_v || - AZStd::is_same_v) + else if constexpr (AZStd::is_same_v || + AZStd::is_same_v) { if (IsServicedByThisDrive(args.m_path.GetAbsolutePath())) { @@ -212,7 +212,7 @@ namespace AZ::IO return; } } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { if (CancelRequest(request, args.m_target)) { @@ -221,15 +221,15 @@ namespace AZ::IO return; } } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushCache(args.m_path); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushEntireCache(); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { Report(args); } @@ -257,13 +257,13 @@ namespace AZ::IO hasWorked = AZStd::visit([this, request](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { FileExistsRequest(request); m_pendingRequests.pop_front(); return true; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FileMetaDataRetrievalRequest(request); m_pendingRequests.pop_front(); @@ -308,7 +308,7 @@ namespace AZ::IO FileReadInformation& read = m_readSlots_readInfo[i]; u64 totalBytesRead = m_readSizeAverage.GetTotal(); double totalReadTimeUSec = aznumeric_caster(m_readTimeAverage.GetTotal().count()); - auto readCommand = AZStd::get_if(&read.m_request->GetCommand()); + auto readCommand = AZStd::get_if(&read.m_request->GetCommand()); AZ_Assert(readCommand, "Request currently reading doesn't contain a read command."); auto endTime = read.m_startTime + AZStd::chrono::microseconds(aznumeric_cast((readCommand->m_size * totalReadTimeUSec) / totalBytesRead)); earliestSlot = AZStd::min(earliestSlot, endTime); @@ -354,25 +354,25 @@ namespace AZ::IO AZStd::visit([&](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { targetFile = &args.m_path; readSize = args.m_size; offset = args.m_offset; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { targetFile = &args.m_compressionInfo.m_archiveFilename; readSize = args.m_compressionInfo.m_compressedSize; offset = args.m_compressionInfo.m_offset; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds getFileExistsTimeAverage = m_getFileExistsTimeAverage.CalculateAverage(); startTime += getFileExistsTimeAverage; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds getFileExistsTimeAverage = m_getFileMetaDataRetrievalTimeAverage.CalculateAverage(); @@ -411,15 +411,15 @@ namespace AZ::IO AZStd::visit([&, this](auto&& args) { using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v || - AZStd::is_same_v) + if constexpr (AZStd::is_same_v || + AZStd::is_same_v) { if (IsServicedByThisDrive(args.m_path.GetAbsolutePath())) { EstimateCompletionTimeForRequest(request, startTime, activeFile, activeOffset); } } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { if (IsServicedByThisDrive(args.m_compressionInfo.m_archiveFilename.GetAbsolutePath())) { @@ -435,7 +435,7 @@ namespace AZ::IO aznumeric_cast(m_pendingRequests.size()) - m_activeReads_Count; } - auto StorageDriveWin::OpenFile(HANDLE& fileHandle, size_t& cacheSlot, FileRequest* request, const FileRequest::ReadData& data) -> OpenFileResult + auto StorageDriveWin::OpenFile(HANDLE& fileHandle, size_t& cacheSlot, FileRequest* request, const Requests::ReadData& data) -> OpenFileResult { HANDLE file = INVALID_HANDLE_VALUE; @@ -553,7 +553,7 @@ namespace AZ::IO return false; } - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); AZ_Assert(data, "Read request in StorageDriveWin doesn't contain read data."); HANDLE file = INVALID_HANDLE_VALUE; @@ -780,7 +780,7 @@ namespace AZ::IO void StorageDriveWin::FileExistsRequest(FileRequest* request) { - auto& fileExists = AZStd::get(request->GetCommand()); + auto& fileExists = AZStd::get(request->GetCommand()); AZ_PROFILE_SCOPE(AzCore, "StorageDriveWin::FileExistsRequest %s : %s", m_name.c_str(), fileExists.m_path.GetRelativePath()); @@ -836,7 +836,7 @@ namespace AZ::IO void StorageDriveWin::FileMetaDataRetrievalRequest(FileRequest* request) { - auto& command = AZStd::get(request->GetCommand()); + auto& command = AZStd::get(request->GetCommand()); AZ_PROFILE_SCOPE(AzCore, "StorageDriveWin::FileMetaDataRetrievalRequest %s : %s", m_name.c_str(), command.m_path.GetRelativePath()); @@ -1005,7 +1005,7 @@ namespace AZ::IO FileReadInformation& fileReadInfo = m_readSlots_readInfo[readSlot]; - auto readCommand = AZStd::get_if(&fileReadInfo.m_request->GetCommand()); + auto readCommand = AZStd::get_if(&fileReadInfo.m_request->GetCommand()); AZ_Assert(readCommand != nullptr, "Request stored with the overlapped I/O call did not contain a read request."); if (fileReadInfo.m_sectorAlignedOutput && !encounteredError) @@ -1147,11 +1147,11 @@ namespace AZ::IO StreamStackEntry::CollectStatistics(statistics); } - void StorageDriveWin::Report(const FileRequest::ReportData& data) const + void StorageDriveWin::Report(const Requests::ReportData& data) const { switch (data.m_reportType) { - case FileRequest::ReportData::ReportType::FileLocks: + case Requests::ReportType::FileLocks: if (m_cachesInitialized) { for (u32 i = 0; i < m_maxFileHandles; ++i) diff --git a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h index c70eb7804d..5207d094ef 100644 --- a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h +++ b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -19,6 +20,12 @@ #include #include +namespace AZ::IO::Requests +{ + struct ReadData; + struct ReportData; +} + namespace AZ::IO { class StorageDriveWin @@ -111,7 +118,7 @@ namespace AZ::IO CacheFull }; - OpenFileResult OpenFile(HANDLE& fileHandle, size_t& cacheSlot, FileRequest* request, const FileRequest::ReadData& data); + OpenFileResult OpenFile(HANDLE& fileHandle, size_t& cacheSlot, FileRequest* request, const Requests::ReadData& data); bool ReadRequest(FileRequest* request); bool ReadRequest(FileRequest* request, size_t readSlot); bool CancelRequest(FileRequest* cancelRequest, FileRequestPtr& target); @@ -137,7 +144,7 @@ namespace AZ::IO void FinalizeSingleRequest(FileReadStatus& status, size_t readSlot, DWORD numBytesTransferred, bool isCanceled, bool encounteredError); - void Report(const FileRequest::ReportData& data) const; + void Report(const Requests::ReportData& data) const; TimedAverageWindow m_fileOpenCloseTimeAverage; TimedAverageWindow m_getFileExistsTimeAverage; diff --git a/Code/Framework/AzCore/Tests/Asset/AssetDataStreamTests.cpp b/Code/Framework/AzCore/Tests/Asset/AssetDataStreamTests.cpp index 27903495f1..3846c6cf16 100644 --- a/Code/Framework/AzCore/Tests/Asset/AssetDataStreamTests.cpp +++ b/Code/Framework/AzCore/Tests/Asset/AssetDataStreamTests.cpp @@ -6,6 +6,7 @@ * */ #include +#include #include #include #include diff --git a/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp b/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp index e312e2058d..95b0626d7f 100644 --- a/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp +++ b/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp @@ -406,7 +406,7 @@ namespace AZ::IO request->CreateFileMetaDataRetrieval(path); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileMetaData = AZStd::get(request.GetCommand()); + auto& fileMetaData = AZStd::get(request.GetCommand()); EXPECT_FALSE(fileMetaData.m_found); EXPECT_EQ(0, fileMetaData.m_fileSize); }); @@ -424,7 +424,7 @@ namespace AZ::IO request->SetCompletionCallback([](const FileRequest& request) { - auto& fileMetaData = AZStd::get(request.GetCommand()); + auto& fileMetaData = AZStd::get(request.GetCommand()); EXPECT_TRUE(fileMetaData.m_found); EXPECT_EQ(4_kib, fileMetaData.m_fileSize); }); @@ -442,7 +442,7 @@ namespace AZ::IO request->CreateFileMetaDataRetrieval(path); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileMetaData = AZStd::get(request.GetCommand()); + auto& fileMetaData = AZStd::get(request.GetCommand()); EXPECT_FALSE(fileMetaData.m_found); EXPECT_EQ(0, fileMetaData.m_fileSize); }); @@ -460,7 +460,7 @@ namespace AZ::IO request->SetCompletionCallback([](const FileRequest& request) { - auto& fileMetaData = AZStd::get(request.GetCommand()); + auto& fileMetaData = AZStd::get(request.GetCommand()); EXPECT_TRUE(fileMetaData.m_found); EXPECT_EQ(16_kib, fileMetaData.m_fileSize); }); @@ -484,7 +484,7 @@ namespace AZ::IO request->SetCompletionCallback([](const FileRequest& request) { - auto& fileMetaData = AZStd::get(request.GetCommand()); + auto& fileMetaData = AZStd::get(request.GetCommand()); EXPECT_TRUE(fileMetaData.m_found); EXPECT_EQ(4_kib, fileMetaData.m_fileSize); }); @@ -502,7 +502,7 @@ namespace AZ::IO request->CreateFileExistsCheck(invalidPath); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileExistsCheck = AZStd::get(request.GetCommand()); + auto& fileExistsCheck = AZStd::get(request.GetCommand()); EXPECT_EQ(AZ::IO::IStreamerTypes::RequestStatus::Completed, request.GetStatus()); EXPECT_FALSE(fileExistsCheck.m_found); }); @@ -519,7 +519,7 @@ namespace AZ::IO request->CreateFileExistsCheck(path); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileExistsCheck = AZStd::get(request.GetCommand()); + auto& fileExistsCheck = AZStd::get(request.GetCommand()); EXPECT_EQ(AZ::IO::IStreamerTypes::RequestStatus::Completed, request.GetStatus()); EXPECT_FALSE(fileExistsCheck.m_found); }); @@ -535,7 +535,7 @@ namespace AZ::IO request->CreateFileExistsCheck(m_dummyRequestPath); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileExistsCheck = AZStd::get(request.GetCommand()); + auto& fileExistsCheck = AZStd::get(request.GetCommand()); EXPECT_EQ(AZ::IO::IStreamerTypes::RequestStatus::Completed, request.GetStatus()); EXPECT_TRUE(fileExistsCheck.m_found); }); @@ -551,7 +551,7 @@ namespace AZ::IO request->CreateFileExistsCheck(m_dummyRequestPath); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileExistsCheck = AZStd::get(request.GetCommand()); + auto& fileExistsCheck = AZStd::get(request.GetCommand()); EXPECT_EQ(AZ::IO::IStreamerTypes::RequestStatus::Completed, request.GetStatus()); EXPECT_TRUE(fileExistsCheck.m_found); }); @@ -573,7 +573,7 @@ namespace AZ::IO request->CreateFileExistsCheck(m_dummyRequestPath); request->SetCompletionCallback([](const FileRequest& request) { - auto& fileExistsCheck = AZStd::get(request.GetCommand()); + auto& fileExistsCheck = AZStd::get(request.GetCommand()); EXPECT_EQ(AZ::IO::IStreamerTypes::RequestStatus::Completed, request.GetStatus()); EXPECT_TRUE(fileExistsCheck.m_found); }); @@ -603,7 +603,7 @@ namespace AZ::IO AZ_POP_DISABLE_WARNING { EXPECT_EQ(request.GetStatus(), AZ::IO::IStreamerTypes::RequestStatus::Completed); - auto& readRequest = AZStd::get(request.GetCommand()); + auto& readRequest = AZStd::get(request.GetCommand()); EXPECT_EQ(readRequest.m_size, fileSize); EXPECT_STREQ(readRequest.m_path.GetAbsolutePath(), m_dummyFilepath.c_str()); }; @@ -648,7 +648,7 @@ namespace AZ::IO AZ_POP_DISABLE_WARNING { EXPECT_EQ(request.GetStatus(), AZ::IO::IStreamerTypes::RequestStatus::Completed); - auto& readRequest = AZStd::get(request.GetCommand()); + auto& readRequest = AZStd::get(request.GetCommand()); EXPECT_EQ(readRequest.m_size, unalignedSize); EXPECT_EQ(readRequest.m_offset, unalignedOffset); EXPECT_STREQ(readRequest.m_path.GetAbsolutePath(), m_dummyFilepath.c_str()); @@ -796,7 +796,7 @@ namespace AZ::IO AZ_POP_DISABLE_WARNING { EXPECT_EQ(request.GetStatus(), AZ::IO::IStreamerTypes::RequestStatus::Completed); - auto& readRequest = AZStd::get(request.GetCommand()); + auto& readRequest = AZStd::get(request.GetCommand()); EXPECT_EQ(readRequest.m_size, chunkSize); EXPECT_EQ(readRequest.m_offset, i * chunkSize); }; diff --git a/Code/Framework/AzCore/Tests/Streamer/BlockCacheTests.cpp b/Code/Framework/AzCore/Tests/Streamer/BlockCacheTests.cpp index 3a107da61d..d9d3623fbd 100644 --- a/Code/Framework/AzCore/Tests/Streamer/BlockCacheTests.cpp +++ b/Code/Framework/AzCore/Tests/Streamer/BlockCacheTests.cpp @@ -100,7 +100,7 @@ namespace AZ::IO void QueueReadRequest(FileRequest* request) { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); if (data) { if (m_fakeFileFound) @@ -122,15 +122,15 @@ namespace AZ::IO m_context->MarkRequestAsCompleted(request); } else if ( - AZStd::holds_alternative(request->GetCommand()) || - AZStd::holds_alternative(request->GetCommand())) + AZStd::holds_alternative(request->GetCommand()) || + AZStd::holds_alternative(request->GetCommand())) { request->SetStatus(IStreamerTypes::RequestStatus::Completed); m_context->MarkRequestAsCompleted(request); } - else if (AZStd::holds_alternative(request->GetCommand())) + else if (AZStd::holds_alternative(request->GetCommand())) { - auto& data2 = AZStd::get(request->GetCommand()); + auto& data2 = AZStd::get(request->GetCommand()); data2.m_found = m_fakeFileFound; data2.m_fileSize = m_fakeFileLength; request->SetStatus(m_fakeFileFound ? IStreamerTypes::RequestStatus::Completed : IStreamerTypes::RequestStatus::Failed); @@ -158,16 +158,16 @@ namespace AZ::IO void QueueCanceledReadRequest(FileRequest* request) { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); if (data) { ReadFile(data->m_output, data->m_path, data->m_offset, data->m_size); request->SetStatus(IStreamerTypes::RequestStatus::Canceled); m_context->MarkRequestAsCompleted(request); } - else if (AZStd::holds_alternative(request->GetCommand())) + else if (AZStd::holds_alternative(request->GetCommand())) { - auto& data2 = AZStd::get(request->GetCommand()); + auto& data2 = AZStd::get(request->GetCommand()); data2.m_found = true; data2.m_fileSize = m_fakeFileLength; request->SetStatus(IStreamerTypes::RequestStatus::Completed); diff --git a/Code/Framework/AzCore/Tests/Streamer/FullDecompressorTests.cpp b/Code/Framework/AzCore/Tests/Streamer/FullDecompressorTests.cpp index 9f2f0dbd6b..f465a0dafb 100644 --- a/Code/Framework/AzCore/Tests/Streamer/FullDecompressorTests.cpp +++ b/Code/Framework/AzCore/Tests/Streamer/FullDecompressorTests.cpp @@ -145,7 +145,7 @@ namespace AZ::IO void PrepareReadRequest(FileRequest* request) { - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); ASSERT_NE(nullptr, data); u64 size = data->m_size >> 2; diff --git a/Code/Framework/AzCore/Tests/Streamer/IStreamerMock.h b/Code/Framework/AzCore/Tests/Streamer/IStreamerMock.h index 7b784caffd..0059a25b93 100644 --- a/Code/Framework/AzCore/Tests/Streamer/IStreamerMock.h +++ b/Code/Framework/AzCore/Tests/Streamer/IStreamerMock.h @@ -9,6 +9,7 @@ #include #include +#include using namespace AZ::IO; diff --git a/Code/Framework/AzCore/Tests/Streamer/ReadSplitterTests.cpp b/Code/Framework/AzCore/Tests/Streamer/ReadSplitterTests.cpp index a68ce091b8..01fe2f6a12 100644 --- a/Code/Framework/AzCore/Tests/Streamer/ReadSplitterTests.cpp +++ b/Code/Framework/AzCore/Tests/Streamer/ReadSplitterTests.cpp @@ -155,7 +155,7 @@ namespace AZ::IO { EXPECT_EQ(subRequests[i]->GetParent(), readRequest); - FileRequest::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); + Requests::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); ASSERT_NE(nullptr, data); EXPECT_EQ(SplitSize, data->m_size); EXPECT_EQ(SplitSize * i, data->m_offset); @@ -210,7 +210,7 @@ namespace AZ::IO { EXPECT_EQ(subRequests[i]->GetParent(), readRequest); - FileRequest::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); + Requests::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); ASSERT_NE(nullptr, data); EXPECT_EQ(SplitSize, data->m_size); EXPECT_EQ(SplitSize * i, data->m_offset); @@ -230,7 +230,7 @@ namespace AZ::IO { EXPECT_EQ(subRequests[i]->GetParent(), readRequest); - FileRequest::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); + Requests::ReadData* data = AZStd::get_if(&subRequests[i]->GetCommand()); ASSERT_NE(nullptr, data); EXPECT_EQ(SplitSize, data->m_size); EXPECT_EQ(SplitSize * (batchSize + i), data->m_offset); @@ -265,7 +265,7 @@ namespace AZ::IO m_readSplitter->QueueRequest(readRequest); ASSERT_NE(nullptr, subRequest); - FileRequest::ReadData* data = AZStd::get_if(&subRequest->GetCommand()); + Requests::ReadData* data = AZStd::get_if(&subRequest->GetCommand()); EXPECT_NE(buffer, data->m_output); EXPECT_EQ(readSize, data->m_size); EXPECT_EQ(0, data->m_offset); @@ -311,7 +311,7 @@ namespace AZ::IO m_readSplitter->QueueRequest(readRequest); ASSERT_NE(nullptr, subRequest); - FileRequest::ReadData* data = AZStd::get_if(&subRequest->GetCommand()); + Requests::ReadData* data = AZStd::get_if(&subRequest->GetCommand()); EXPECT_NE(buffer, data->m_output); EXPECT_EQ(readSize + offsetAdjustment, data->m_size); EXPECT_EQ(0, data->m_offset); diff --git a/Code/Framework/AzCore/Tests/Streamer/SchedulerTests.cpp b/Code/Framework/AzCore/Tests/Streamer/SchedulerTests.cpp index 6c360b97de..d1484b7409 100644 --- a/Code/Framework/AzCore/Tests/Streamer/SchedulerTests.cpp +++ b/Code/Framework/AzCore/Tests/Streamer/SchedulerTests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -86,7 +87,7 @@ namespace AZ::IO .WillOnce([this](FileRequest* request) { AZ_Assert(m_streamerContext, "AZ::IO::Streamer is not ready to process requests."); - auto readData = AZStd::get_if(&request->GetCommand()); + auto readData = AZStd::get_if(&request->GetCommand()); AZ_Assert(readData, "Test didn't pass in the correct request."); FileRequest* read = m_streamerContext->GetNewInternalRequest(); read->CreateRead(request, readData->m_output, readData->m_outputSize, readData->m_path, @@ -99,7 +100,7 @@ namespace AZ::IO .WillOnce([this](FileRequest* request) { AZ_Assert(m_streamerContext, "AZ::IO::Streamer is not ready to process requests."); - auto readData = AZStd::get_if(&request->GetCommand()); + auto readData = AZStd::get_if(&request->GetCommand()); AZ_Assert(readData, "Test didn't pass in the correct request."); auto output = reinterpret_cast(readData->m_output); AZ_Assert(output != nullptr, "Output buffer has not been set."); @@ -304,7 +305,7 @@ namespace AZ::IO EXPECT_CALL(*m_mock, QueueRequest(_)).Times(1) .WillOnce(Invoke([this](FileRequest* request) { - auto* read = request->GetCommandFromChain(); + auto* read = request->GetCommandFromChain(); ASSERT_NE(nullptr, read); EXPECT_LT(read->m_deadline, FileRequest::s_noDeadlineTime); EXPECT_EQ(read->m_priority, IStreamerTypes::s_priorityHighest); diff --git a/Code/Framework/AzCore/Tests/Streamer/StreamStackEntryConformityTests.h b/Code/Framework/AzCore/Tests/Streamer/StreamStackEntryConformityTests.h index 6cbec7ea8a..8a5805cccd 100644 --- a/Code/Framework/AzCore/Tests/Streamer/StreamStackEntryConformityTests.h +++ b/Code/Framework/AzCore/Tests/Streamer/StreamStackEntryConformityTests.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/Code/Framework/AzCore/Tests/StreamerTests.cpp b/Code/Framework/AzCore/Tests/StreamerTests.cpp index 78f9f9060f..937b6d29de 100644 --- a/Code/Framework/AzCore/Tests/StreamerTests.cpp +++ b/Code/Framework/AzCore/Tests/StreamerTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/Code/Framework/AzFramework/AzFramework/Asset/AssetSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Asset/AssetSystemComponent.cpp index bb361e56de..35c23d119a 100644 --- a/Code/Framework/AzFramework/AzFramework/Asset/AssetSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Asset/AssetSystemComponent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.cpp b/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.cpp index 589c805871..23d275cb78 100644 --- a/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.cpp +++ b/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.cpp @@ -83,9 +83,9 @@ namespace AzFramework AZ_PROFILE_FUNCTION(AzCore); AZ_Assert(request, "PrepareRequest was provided a null request."); - if (AZStd::holds_alternative(request->GetCommand())) + if (AZStd::holds_alternative(request->GetCommand())) { - auto& readRequest = AZStd::get(request->GetCommand()); + auto& readRequest = AZStd::get(request->GetCommand()); FileRequest* read = m_context->GetNewInternalRequest(); read->CreateRead(request, readRequest.m_output, readRequest.m_outputSize, readRequest.m_path, @@ -106,14 +106,14 @@ namespace AzFramework { using namespace AZ::IO; using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v || - AZStd::is_same_v || - AZStd::is_same_v) + if constexpr (AZStd::is_same_v || + AZStd::is_same_v || + AZStd::is_same_v) { m_pendingRequests.push_back(request); return; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { if (CancelRequest(request, args.m_target)) { @@ -124,15 +124,15 @@ namespace AzFramework } else { - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { FlushCache(args.m_path); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FlushEntireCache(); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { Report(args); } @@ -152,15 +152,15 @@ namespace AzFramework { using namespace AZ::IO; using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { ReadFile(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FileExistsRequest(request); } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { FileMetaDataRetrievalRequest(request); } @@ -232,23 +232,23 @@ namespace AzFramework { using namespace AZ::IO; using Command = AZStd::decay_t; - if constexpr (AZStd::is_same_v) + if constexpr (AZStd::is_same_v) { targetFile = &args.m_path; readSize = args.m_size; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { targetFile = &args.m_compressionInfo.m_archiveFilename; readSize = args.m_compressionInfo.m_compressedSize; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds averageTime = m_getFileExistsTimeAverage.CalculateAverage(); startTime += averageTime; } - else if constexpr (AZStd::is_same_v) + else if constexpr (AZStd::is_same_v) { readSize = 0; AZStd::chrono::microseconds averageTime = m_getFileMetaDataTimeAverage.CalculateAverage(); @@ -280,7 +280,7 @@ namespace AzFramework AZ_PROFILE_FUNCTION(AzCore); - auto data = AZStd::get_if(&request->GetCommand()); + auto data = AZStd::get_if(&request->GetCommand()); AZ_Assert(data, "Request doing reading in the RemoteStorageDrive didn't contain read data."); HandleType file = InvalidHandle; @@ -397,7 +397,7 @@ namespace AzFramework TIMED_AVERAGE_WINDOW_SCOPE(m_getFileExistsTimeAverage); - auto& fileExists = AZStd::get(request->GetCommand()); + auto& fileExists = AZStd::get(request->GetCommand()); size_t cacheIndex = FindFileInCache(fileExists.m_path); if (cacheIndex != s_fileNotFound) { @@ -430,7 +430,7 @@ namespace AzFramework AZ::u64 fileSize = 0; bool found = false; - auto& command = AZStd::get(request->GetCommand()); + auto& command = AZStd::get(request->GetCommand()); // If the file is already open, use the file handle which usually is cheaper than asking for the file by name. size_t cacheIndex = FindFileInCache(command.m_path); if (cacheIndex != s_fileNotFound) @@ -526,13 +526,13 @@ namespace AzFramework StreamStackEntry::CollectStatistics(statistics); } - void RemoteStorageDrive::Report(const AZ::IO::FileRequest::ReportData& data) const + void RemoteStorageDrive::Report(const AZ::IO::Requests::ReportData& data) const { using namespace AZ::IO; switch (data.m_reportType) { - case FileRequest::ReportData::ReportType::FileLocks: + case Requests::ReportType::FileLocks: for (AZ::u32 i = 0; i < m_fileHandles.size(); ++i) { if (m_fileHandles[i] != InvalidHandle) diff --git a/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.h b/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.h index c3e28f2e55..de0b855dcc 100644 --- a/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.h +++ b/Code/Framework/AzFramework/AzFramework/IO/RemoteStorageDrive.h @@ -16,6 +16,15 @@ #include #include +namespace AZ::IO +{ + class RequestPath; + namespace Requests + { + struct ReportData; + } +} + namespace AzFramework { struct RemoteStorageDriveConfig final : @@ -63,7 +72,7 @@ namespace AzFramework const AZ::IO::RequestPath*& activeFile) const; void FlushCache(const AZ::IO::RequestPath& filePath); void FlushEntireCache(); - void Report(const AZ::IO::FileRequest::ReportData& data) const; + void Report(const AZ::IO::Requests::ReportData& data) const; AZ::IO::RemoteFileIO m_fileIO; AZ::IO::TimedAverageWindow m_fileOpenCloseTimeAverage; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBody.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBody.cpp index 168152f686..bb1270a387 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBody.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBody.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyAutomation.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyAutomation.cpp index e78ff49f2d..a86ef12216 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyAutomation.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyAutomation.cpp @@ -8,6 +8,7 @@ #include +#include #include #include diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyEvents.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyEvents.cpp index b51298a797..005ce291f6 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyEvents.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Common/PhysicsSimulatedBodyEvents.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include diff --git a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.cpp b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.cpp index a5ebdd04c6..1937c50555 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsScene.cpp @@ -9,6 +9,7 @@ #include #include +#include #include diff --git a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsSystem.cpp b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsSystem.cpp index 2ff4780c9e..a0bb2d9366 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/PhysicsSystem.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/PhysicsSystem.cpp @@ -8,6 +8,7 @@ #include +#include #include namespace AzPhysics diff --git a/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp b/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp index 79365f1ebe..b2c36bc1c8 100644 --- a/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Script/ScriptComponent.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/ToolsAssetCatalogComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/ToolsAssetCatalogComponent.h index a68b82468e..56a3e7005c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/ToolsAssetCatalogComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/ToolsAssetCatalogComponent.h @@ -7,6 +7,7 @@ */ #pragma once +#include #include #include #include diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/Model/AssetCompleterModel.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/Model/AssetCompleterModel.h index 4596d70657..34b3a0a0b6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/Model/AssetCompleterModel.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/Model/AssetCompleterModel.h @@ -10,12 +10,13 @@ #if !defined(Q_MOC_RUN) #include +#include #endif namespace AzToolsFramework { using namespace AzToolsFramework::AssetBrowser; - + //! Model storing all the files that can be suggested in the Asset Autocompleter for PropertyAssetCtrl class AssetCompleterModel : public QAbstractTableModel @@ -45,7 +46,7 @@ namespace AzToolsFramework void SetFetchEntryType(AssetBrowserEntry::AssetEntryType entryType); private: - struct AssetItem + struct AssetItem { AZStd::string m_displayName; AZStd::string m_path; diff --git a/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderComponent.cpp b/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderComponent.cpp index 78244ab02c..78e71ab3f2 100644 --- a/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderComponent.cpp +++ b/Code/Tools/AssetProcessor/AssetBuilder/AssetBuilderComponent.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h index 982de92739..838cdd2256 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h @@ -7,13 +7,15 @@ */ #pragma once -#include -#include #include #include #include #include +#include +#include +#include + namespace AZ { class ReflectContext; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Shader/PrecompiledShaderAssetSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Shader/PrecompiledShaderAssetSourceData.h index ce5a4c8a7a..701b90ace9 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Shader/PrecompiledShaderAssetSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Shader/PrecompiledShaderAssetSourceData.h @@ -10,6 +10,7 @@ #include #include +#include namespace AZ { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp index bd196a2e2b..b97ffecccd 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp @@ -267,7 +267,7 @@ namespace AZ return m_visScene->GetEntryCount(); } - + struct WorklistData { CullingDebugContext* m_debugCtx = nullptr; @@ -296,13 +296,13 @@ namespace AZ #endif return worklistData; } - + constexpr size_t WorkListCapacity = 5; using WorkListType = AZStd::fixed_vector; #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED static MaskedOcclusionCulling::CullingResult TestOcclusionCulling( - const AZStd::shared_ptr& worklistData, + const AZStd::shared_ptr& worklistData, AzFramework::VisibilityEntry* visibleEntry); #endif @@ -320,8 +320,8 @@ namespace AZ for (const AzFramework::IVisibilityScene::NodeData& nodeData : worklist) { //If a node is entirely contained within the frustum, then we can skip the fine grained culling. - bool nodeIsContainedInFrustum = - !worklistData->m_debugCtx->m_enableFrustumCulling || + bool nodeIsContainedInFrustum = + !worklistData->m_debugCtx->m_enableFrustumCulling || ShapeIntersection::Contains(worklistData->m_frustum, nodeData.m_bounds); #ifdef AZ_CULL_PROFILE_VERBOSE @@ -460,12 +460,14 @@ namespace AZ cullStats.m_numVisibleCullables += numVisibleCullables; ++cullStats.m_numJobs; } +#else + (void)numDrawPackets; // prevent unused variable warning->error #endif //AZ_CULL_DEBUG_ENABLED } #if AZ_TRAIT_MASKED_OCCLUSION_CULLING_SUPPORTED static MaskedOcclusionCulling::CullingResult TestOcclusionCulling( - const AZStd::shared_ptr& worklistData, + const AZStd::shared_ptr& worklistData, AzFramework::VisibilityEntry* visibleEntry) { if (!worklistData->m_maskedOcclusionCulling) @@ -527,9 +529,9 @@ namespace AZ #endif void CullingScene::ProcessCullablesCommon( - const Scene& scene [[maybe_unused]], - View& view, - AZ::Frustum& frustum [[maybe_unused]], + const Scene& scene [[maybe_unused]], + View& view, + AZ::Frustum& frustum [[maybe_unused]], void*& maskedOcclusionCulling [[maybe_unused]]) { AZ_PROFILE_SCOPE(RPI, "CullingScene::ProcessCullablesCommon() - %s", view.GetName().GetCStr()); @@ -898,7 +900,7 @@ namespace AZ { const Matrix4x4& worldToClip = viewPtr->GetWorldToClipMatrix(); Frustum frustum = Frustum::CreateFromMatrixColumnMajor(worldToClip, Frustum::ReverseDepth::True); - m_debugCtx.m_frozenFrustums.insert({ viewPtr.get(), frustum }); + m_debugCtx.m_frozenFrustums.insert({ viewPtr.get(), frustum }); } } } @@ -911,7 +913,7 @@ namespace AZ } void CullingScene::EndCulling() - { + { m_cullDataConcurrencyCheck.soft_unlock(); } diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp index e11624a921..460e671b4c 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp @@ -11,13 +11,13 @@ #include +#include +#include #include #include #include - #include -#include namespace AZ { diff --git a/Gems/Atom/RPI/Code/Tests/Common/AssetManagerTestFixture.cpp b/Gems/Atom/RPI/Code/Tests/Common/AssetManagerTestFixture.cpp index fd19c6bcb8..83e8dbf085 100644 --- a/Gems/Atom/RPI/Code/Tests/Common/AssetManagerTestFixture.cpp +++ b/Gems/Atom/RPI/Code/Tests/Common/AssetManagerTestFixture.cpp @@ -7,6 +7,8 @@ */ #include "AssetManagerTestFixture.h" + +#include #include #include diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/AssetCollectionAsyncLoader.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/AssetCollectionAsyncLoader.h index d1027daa36..b974b4443d 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/AssetCollectionAsyncLoader.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/AssetCollectionAsyncLoader.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp index 58b3d8b56e..35ae1b1257 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Source/AtomActorInstance.cpp @@ -38,6 +38,8 @@ #include #include +#include + namespace AZ::Render { static constexpr uint32_t s_maxActiveWrinkleMasks = 16; diff --git a/Gems/AtomTressFX/Code/Rendering/SharedBuffer.cpp b/Gems/AtomTressFX/Code/Rendering/SharedBuffer.cpp index b31d9f8ae0..6f97613c89 100644 --- a/Gems/AtomTressFX/Code/Rendering/SharedBuffer.cpp +++ b/Gems/AtomTressFX/Code/Rendering/SharedBuffer.cpp @@ -16,6 +16,8 @@ #include #include +#include + namespace AZ::Render { //! Setting the constructor as private will create compile error to remind the developer to set diff --git a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp index 2d95ddf4dd..ce17803531 100644 --- a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp +++ b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp @@ -8,6 +8,7 @@ #include +#include namespace Audio { @@ -285,5 +286,22 @@ namespace Audio return sResult; } + CATLAudioFileEntry::CATLAudioFileEntry(const char * const filePath, IATLAudioFileEntryData * const implData) + : m_filePath(filePath) + , m_fileSize(0) + , m_useCount(0) + , m_memoryBlockAlignment(AUDIO_MEMORY_ALIGNMENT) + , m_flags(eAFF_NOTFOUND) + , m_dataScope(eADS_ALL) + , m_memoryBlock(nullptr) + , m_implData(implData) + { + } + + CATLAudioFileEntry::~CATLAudioFileEntry() + { + + } + #endif // !AUDIO_RELEASE } // namespace Audio diff --git a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.h b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.h index f1ff74c370..4dfceeebfc 100644 --- a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.h +++ b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.h @@ -379,19 +379,9 @@ namespace Audio class CATLAudioFileEntry { public: - explicit CATLAudioFileEntry(const char* const filePath = nullptr, IATLAudioFileEntryData* const implData = nullptr) - : m_filePath(filePath) - , m_fileSize(0) - , m_useCount(0) - , m_memoryBlockAlignment(AUDIO_MEMORY_ALIGNMENT) - , m_flags(eAFF_NOTFOUND) - , m_dataScope(eADS_ALL) - , m_memoryBlock(nullptr) - , m_implData(implData) - { - } + explicit CATLAudioFileEntry(const char* const filePath = nullptr, IATLAudioFileEntryData* const implData = nullptr); - ~CATLAudioFileEntry() = default; + ~CATLAudioFileEntry(); AZStd::string m_filePath; size_t m_fileSize; diff --git a/Gems/AudioSystem/Code/Source/Engine/FileCacheManager.cpp b/Gems/AudioSystem/Code/Source/Engine/FileCacheManager.cpp index ffa94ad956..d1f2187942 100644 --- a/Gems/AudioSystem/Code/Source/Engine/FileCacheManager.cpp +++ b/Gems/AudioSystem/Code/Source/Engine/FileCacheManager.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include diff --git a/Gems/AudioSystem/Code/Tests/AudioSystemTest.cpp b/Gems/AudioSystem/Code/Tests/AudioSystemTest.cpp index 14e9f74fd2..a197fee96b 100644 --- a/Gems/AudioSystem/Code/Tests/AudioSystemTest.cpp +++ b/Gems/AudioSystem/Code/Tests/AudioSystemTest.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/Gems/AudioSystem/Code/Tests/Mocks/FileCacheManagerMock.h b/Gems/AudioSystem/Code/Tests/Mocks/FileCacheManagerMock.h index 06646216ae..1fb48bb3fb 100644 --- a/Gems/AudioSystem/Code/Tests/Mocks/FileCacheManagerMock.h +++ b/Gems/AudioSystem/Code/Tests/Mocks/FileCacheManagerMock.h @@ -13,6 +13,11 @@ #include #include +namespace AZ::IO +{ + class FileRequestHandle; +} + namespace Audio { class FileCacheManagerMock diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/ActorInstance.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/ActorInstance.cpp index c196f34716..bde1259c44 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/ActorInstance.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/ActorInstance.cpp @@ -34,6 +34,8 @@ #include #include +#include + namespace EMotionFX { AZ_CLASS_ALLOCATOR_IMPL(ActorInstance, ActorInstanceAllocator, 0) diff --git a/Gems/EMotionFX/Code/Tests/TestAssetCode/SimpleActors.cpp b/Gems/EMotionFX/Code/Tests/TestAssetCode/SimpleActors.cpp index 3e1dac41c2..e6415817cc 100644 --- a/Gems/EMotionFX/Code/Tests/TestAssetCode/SimpleActors.cpp +++ b/Gems/EMotionFX/Code/Tests/TestAssetCode/SimpleActors.cpp @@ -15,6 +15,8 @@ #include #include +#include + namespace EMotionFX { SimpleJointChainActor::SimpleJointChainActor(size_t jointCount, const char* name) diff --git a/Gems/LmbrCentral/Code/Source/Asset/AssetSystemDebugComponent.cpp b/Gems/LmbrCentral/Code/Source/Asset/AssetSystemDebugComponent.cpp index acd1bc8dae..91ba63ddd1 100644 --- a/Gems/LmbrCentral/Code/Source/Asset/AssetSystemDebugComponent.cpp +++ b/Gems/LmbrCentral/Code/Source/Asset/AssetSystemDebugComponent.cpp @@ -9,7 +9,9 @@ #include "AssetSystemDebugComponent.h" #include "ISystem.h" #include "IRenderAuxGeom.h" + #include "AzCore/Asset/AssetManager.h" +#include #include #include #include diff --git a/Gems/LmbrCentral/Code/Source/Audio/AudioAreaEnvironmentComponent.cpp b/Gems/LmbrCentral/Code/Source/Audio/AudioAreaEnvironmentComponent.cpp index 827e20b02b..3b14822076 100644 --- a/Gems/LmbrCentral/Code/Source/Audio/AudioAreaEnvironmentComponent.cpp +++ b/Gems/LmbrCentral/Code/Source/Audio/AudioAreaEnvironmentComponent.cpp @@ -8,6 +8,7 @@ #include "AudioAreaEnvironmentComponent.h" +#include #include #include #include diff --git a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothColliders.h b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothColliders.h index a400f742de..baac5b3776 100644 --- a/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothColliders.h +++ b/Gems/NvCloth/Code/Source/Components/ClothComponentMesh/ActorClothColliders.h @@ -10,6 +10,7 @@ #include #include +#include #include namespace NvCloth diff --git a/Gems/NvCloth/Code/Source/Utils/AssetHelper.h b/Gems/NvCloth/Code/Source/Utils/AssetHelper.h index e3e5a5e7ad..4a40fe26f0 100644 --- a/Gems/NvCloth/Code/Source/Utils/AssetHelper.h +++ b/Gems/NvCloth/Code/Source/Utils/AssetHelper.h @@ -10,6 +10,7 @@ #include #include +#include #include diff --git a/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeAngleCone.h b/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeAngleCone.h index 3f6d001112..11215ef29b 100644 --- a/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeAngleCone.h +++ b/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeAngleCone.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeSnap.h b/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeSnap.h index 843fffd4c8..c7a50c43f5 100644 --- a/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeSnap.h +++ b/Gems/PhysX/Code/Editor/Source/ComponentModes/Joints/JointsSubComponentModeSnap.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include diff --git a/Gems/PhysX/Code/Source/ForceRegionComponent.cpp b/Gems/PhysX/Code/Source/ForceRegionComponent.cpp index f8f5f1991e..eba902049d 100644 --- a/Gems/PhysX/Code/Source/ForceRegionComponent.cpp +++ b/Gems/PhysX/Code/Source/ForceRegionComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp index 975fec2181..c02324ed18 100644 --- a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp +++ b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.cpp @@ -6,18 +6,22 @@ * */ -#include -#include -#include -#include +#include #include #include #include #include -#include #include +#include +#include +#include +#include +#include + + + namespace PhysX::Utils { struct PxJointActorData diff --git a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h index da7b3dc4e2..d5aa082763 100644 --- a/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h +++ b/Gems/PhysX/Code/Source/Joint/PhysXJointUtils.h @@ -14,8 +14,15 @@ #include +#include + namespace PhysX { + struct D6JointLimitConfiguration; + struct FixedJointConfiguration; + struct BallJointConfiguration; + struct HingeJointConfiguration; + namespace JointConstants { // Setting joint limits to very small values can cause extreme stability problems, so clamp above a small diff --git a/Gems/PhysX/Code/Source/JointComponent.cpp b/Gems/PhysX/Code/Source/JointComponent.cpp index d5c04598a5..085fc34b12 100644 --- a/Gems/PhysX/Code/Source/JointComponent.cpp +++ b/Gems/PhysX/Code/Source/JointComponent.cpp @@ -5,17 +5,18 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ +#include +#include +#include +#include +#include +#include #include #include #include #include -#include -#include -#include -#include #include -#include namespace PhysX { diff --git a/Gems/PhysX/Code/Source/Material.cpp b/Gems/PhysX/Code/Source/Material.cpp index be79728cc5..fa131b5159 100644 --- a/Gems/PhysX/Code/Source/Material.cpp +++ b/Gems/PhysX/Code/Source/Material.cpp @@ -7,9 +7,10 @@ */ #include "Material.h" +#include #include -#include #include +#include #include namespace PhysX diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp index f4cdd01889..fbadb8aa79 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/CharacterUtils.cpp @@ -5,13 +5,10 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - #include + #include #include -#include -#include -#include #include #include #include @@ -20,6 +17,12 @@ #include #include +#include +#include +#include + +#include + namespace PhysX::Utils::Characters { AZ::Outcome GetNodeIndex(const Physics::RagdollConfiguration& configuration, const AZStd::string& nodeName) diff --git a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp index 34fe3d0295..3e240f3611 100644 --- a/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp +++ b/Gems/PhysX/Code/Source/PhysXCharacters/API/RagdollNode.cpp @@ -5,16 +5,17 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ - -#include -#include -#include #include + #include #include #include - #include +#include +#include +#include +#include + namespace PhysX { diff --git a/Gems/PhysX/Code/Source/Pipeline/HeightFieldAssetHandler.cpp b/Gems/PhysX/Code/Source/Pipeline/HeightFieldAssetHandler.cpp index 2d05612d74..ece6acbd6f 100644 --- a/Gems/PhysX/Code/Source/Pipeline/HeightFieldAssetHandler.cpp +++ b/Gems/PhysX/Code/Source/Pipeline/HeightFieldAssetHandler.cpp @@ -6,15 +6,16 @@ * */ +#include #include #include +#include #include #include #include #include #include -#include #include #include diff --git a/Gems/PhysX/Code/Source/Pipeline/StreamWrapper.h b/Gems/PhysX/Code/Source/Pipeline/StreamWrapper.h index 15f9bfb36e..d987b7194c 100644 --- a/Gems/PhysX/Code/Source/Pipeline/StreamWrapper.h +++ b/Gems/PhysX/Code/Source/Pipeline/StreamWrapper.h @@ -10,6 +10,7 @@ #include #include +#include #include namespace PhysX diff --git a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp index dfac43b703..6c02697e90 100644 --- a/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp +++ b/Gems/PhysX/Code/Source/Scene/PhysXScene.cpp @@ -7,14 +7,6 @@ */ #include -#include -#include -#include -#include -#include -#include -#include -#include #include #include @@ -31,6 +23,16 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace PhysX { AZ_CLASS_ALLOCATOR_IMPL(PhysXScene, AZ::SystemAllocator, 0); diff --git a/Gems/PhysX/Code/Source/System/PhysXSystem.cpp b/Gems/PhysX/Code/Source/System/PhysXSystem.cpp index 168adc8910..d7ce797b47 100644 --- a/Gems/PhysX/Code/Source/System/PhysXSystem.cpp +++ b/Gems/PhysX/Code/Source/System/PhysXSystem.cpp @@ -5,18 +5,20 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ -#include +#include + +#include +#include +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include - -#include +#include +#include +#include // only enable physx timestep warning when not running debug or in Release #if !defined(DEBUG) && !defined(RELEASE) diff --git a/Gems/PhysX/Code/Tests/PhysXTestUtil.cpp b/Gems/PhysX/Code/Tests/PhysXTestUtil.cpp index 2366f65e07..f79b7c8d34 100644 --- a/Gems/PhysX/Code/Tests/PhysXTestUtil.cpp +++ b/Gems/PhysX/Code/Tests/PhysXTestUtil.cpp @@ -9,6 +9,7 @@ #include "PhysXTestUtil.h" #include #include +#include namespace PhysX { diff --git a/Gems/PhysXDebug/Code/Source/SystemComponent.cpp b/Gems/PhysXDebug/Code/Source/SystemComponent.cpp index 65115f2790..746809a7ca 100644 --- a/Gems/PhysXDebug/Code/Source/SystemComponent.cpp +++ b/Gems/PhysXDebug/Code/Source/SystemComponent.cpp @@ -8,10 +8,7 @@ #include "SystemComponent.h" -#include -#include -#include -#include +#include #include #include @@ -19,20 +16,24 @@ #include #include +#include +#include +#include +#include + +#include #include #include #include #include #include -#include -#include -#include +#include +#include +#include +#include +#include -#include -#include - -#include namespace PhysXDebug { diff --git a/Gems/ScriptCanvas/Code/Editor/Assets/ScriptCanvasMemoryAsset.cpp b/Gems/ScriptCanvas/Code/Editor/Assets/ScriptCanvasMemoryAsset.cpp index 439f9a8383..d9ec1de757 100644 --- a/Gems/ScriptCanvas/Code/Editor/Assets/ScriptCanvasMemoryAsset.cpp +++ b/Gems/ScriptCanvas/Code/Editor/Assets/ScriptCanvasMemoryAsset.cpp @@ -10,11 +10,14 @@ #include "ScriptCanvasMemoryAsset.h" #include "ScriptCanvasUndoHelper.h" -#include +#include +#include +#include #include -#include -#include #include +#include +#include +#include namespace ScriptCanvasEditor { diff --git a/Gems/ScriptCanvas/Code/Editor/View/Windows/Tools/UpgradeTool/FileSaver.cpp b/Gems/ScriptCanvas/Code/Editor/View/Windows/Tools/UpgradeTool/FileSaver.cpp index 49a35eac8a..95b4fbb843 100644 --- a/Gems/ScriptCanvas/Code/Editor/View/Windows/Tools/UpgradeTool/FileSaver.cpp +++ b/Gems/ScriptCanvas/Code/Editor/View/Windows/Tools/UpgradeTool/FileSaver.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Gems/ScriptEvents/Code/Source/ScriptEventsSystemComponent.h b/Gems/ScriptEvents/Code/Source/ScriptEventsSystemComponent.h index 64a0e49bca..922bfb1dbc 100644 --- a/Gems/ScriptEvents/Code/Source/ScriptEventsSystemComponent.h +++ b/Gems/ScriptEvents/Code/Source/ScriptEventsSystemComponent.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include diff --git a/Gems/WhiteBox/Code/Source/Viewport/WhiteBoxVertexTranslationModifier.h b/Gems/WhiteBox/Code/Source/Viewport/WhiteBoxVertexTranslationModifier.h index a9565aedbe..d9bae70a31 100644 --- a/Gems/WhiteBox/Code/Source/Viewport/WhiteBoxVertexTranslationModifier.h +++ b/Gems/WhiteBox/Code/Source/Viewport/WhiteBoxVertexTranslationModifier.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include From 90f710b8c2564547293487a53af0b474572a8481 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sun, 31 Oct 2021 09:48:06 -0700 Subject: [PATCH 02/37] feat: add cursor wrapped mode Signed-off-by: Michael Pollind --- Code/Editor/EditorViewportSettings.cpp | 11 ++ Code/Editor/EditorViewportSettings.h | 3 + .../test_ModularViewportCameraController.cpp | 4 +- .../Input/QtEventToAzInputMapper.cpp | 109 ++++++++++++++---- .../Input/QtEventToAzInputMapper.h | 19 ++- .../Source/Viewport/RenderViewportWidget.cpp | 4 +- 6 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index e06b9696e1..5711d1c3d3 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -23,6 +23,7 @@ namespace SandboxEditor constexpr AZStd::string_view AngleSizeSetting = "/Amazon/Preferences/Editor/AngleSize"; constexpr AZStd::string_view ShowGridSetting = "/Amazon/Preferences/Editor/ShowGrid"; constexpr AZStd::string_view StickySelectSetting = "/Amazon/Preferences/Editor/StickySelect"; + constexpr AZStd::string_view ManipulatorMouseWrapSetting = "/Amazon/Preferences/Editor/Manipulator/MouseWrapping"; constexpr AZStd::string_view ManipulatorLineBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/LineBoundWidth"; constexpr AZStd::string_view ManipulatorCircleBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/CircleBoundWidth"; constexpr AZStd::string_view CameraTranslateSpeedSetting = "/Amazon/Preferences/Editor/Camera/TranslateSpeed"; @@ -186,6 +187,16 @@ namespace SandboxEditor AzToolsFramework::SetRegistry(ManipulatorLineBoundWidthSetting, lineBoundWidth); } + bool ManipulatorMouseWrap() + { + return aznumeric_cast(GetRegistry(ManipulatorMouseWrapSetting, false)); + } + + void SetManipulatorMouseWrap(bool wrapping) + { + SetRegistry(ManipulatorMouseWrapSetting, wrapping); + } + float ManipulatorCircleBoundWidth() { return aznumeric_cast(AzToolsFramework::GetRegistry(ManipulatorCircleBoundWidthSetting, 0.1)); diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index fe1253ed0c..975feff307 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -57,6 +57,9 @@ namespace SandboxEditor SANDBOX_API float ManipulatorLineBoundWidth(); SANDBOX_API void SetManipulatorLineBoundWidth(float lineBoundWidth); + SANDBOX_API bool ManipulatorMouseWrap(); + SANDBOX_API void SetManipulatorMouseWrap(bool wrapping); + SANDBOX_API float ManipulatorCircleBoundWidth(); SANDBOX_API void SetManipulatorCircleBoundWidth(float circleBoundWidth); diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index 67d89ad967..f2ee3c79aa 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -47,12 +47,12 @@ namespace UnitTest void ViewportMouseCursorRequestImpl::BeginCursorCapture() { - m_inputChannelMapper->SetCursorCaptureEnabled(true); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_CAPTURED); } void ViewportMouseCursorRequestImpl::EndCursorCapture() { - m_inputChannelMapper->SetCursorCaptureEnabled(false); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_NONE); } bool ViewportMouseCursorRequestImpl::IsMouseOver() const diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp index 066bdc1654..0b6df58c1a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp @@ -241,22 +241,39 @@ namespace AzToolsFramework } } + + void QtEventToAzInputMapper::SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode) + { + if(mode != m_cursorMode) + { + m_cursorMode = mode; + switch(m_cursorMode) + { + case CURSOR_MODE_CAPTURED: + qApp->setOverrideCursor(Qt::BlankCursor); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden); + break; + case CURSOR_MODE_WRAPPED: + qApp->restoreOverrideCursor(); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); + break; + case CURSOR_MODE_NONE: + qApp->restoreOverrideCursor(); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); + break; + } + } + } + void QtEventToAzInputMapper::SetCursorCaptureEnabled(bool enabled) { - if (m_capturingCursor != enabled) + if (enabled) { - m_capturingCursor = enabled; - - if (m_capturingCursor) - { - m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden); - qApp->setOverrideCursor(Qt::BlankCursor); - } - else - { - m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); - qApp->restoreOverrideCursor(); - } + SetCursorMode(CURSOR_MODE_CAPTURED); + } + else + { + SetCursorMode(CURSOR_MODE_NONE); } } @@ -436,25 +453,73 @@ namespace AzToolsFramework return QPoint{ denormalizedX, denormalizedY }; } + void wrapCursorX(const QRect& rect, QPoint& point) { + if (rect.left() < point.x()) + { + point.setX(rect.right() - 1); + } + else if (rect.right() > point.x()) + { + point.setX(rect.left() + 1); + } + } + + void wrapCursorY(const QRect& rect, QPoint& point) { + if (rect.top() < point.y()) + { + point.setY(rect.bottom() - 1); + } + else if (rect.bottom() > point.y()) + { + point.setY(rect.top() + 1); + } + } + + void QtEventToAzInputMapper::HandleMouseMoveEvent(const QPoint& globalCursorPosition) { const QPoint cursorDelta = globalCursorPosition - m_previousGlobalCursorPosition; + QScreen* screen = m_sourceWidget->screen(); + const QRect widgetRect(m_sourceWidget->mapToGlobal(QPoint(0,0)), m_sourceWidget->size()); m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(m_sourceWidget->mapFromGlobal(globalCursorPosition)); m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta = WidgetPositionToNormalizedPosition(cursorDelta); - ProcessPendingMouseEvents(cursorDelta); + switch(m_cursorMode) + { + case CURSOR_MODE_CAPTURED: + AzQtComponents::SetCursorPos(m_previousGlobalCursorPosition); + break; + case CURSOR_MODE_WRAPPED_X: + QPoint screenPos(globalCursorPosition); + wrapCursorX(widgetRect, screenPos); + QCursor::setPos(screen, screenPos); + QPoint screenDelta = globalCursorPosition - screenPos; + m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; + break; + case CURSOR_MODE_WRAPPED_Y: + QPoint screenPos(globalCursorPosition); + wrapCursorY(widgetRect, screenPos); + QCursor::setPos(screen, screenPos); + QPoint screenDelta = globalCursorPosition - screenPos; + m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; + break; + case CURSOR_MODE_WRAPPED: + QPoint screenPos(globalCursorPosition); + wrapCursorX(widgetRect, screenPos); + wrapCursorY(widgetRect, screenPos); + QCursor::setPos(screen, screenPos); + QPoint screenDelta = globalCursorPosition - screenPos; + m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; + break; + default: + m_previousGlobalCursorPosition = globalCursorPosition; + break; + - if (m_capturingCursor) - { - // Reset our cursor position to the previous point - AzQtComponents::SetCursorPos(m_previousGlobalCursorPosition); - } - else - { - m_previousGlobalCursorPosition = globalCursorPosition; } + ProcessPendingMouseEvents(cursorDelta); } void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h index 373945d445..7ba4e21bd4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h @@ -41,6 +41,17 @@ namespace AzToolsFramework Q_OBJECT public: + enum CursorInputMode { + CURSOR_MODE_NONE, + CURSOR_MODE_CAPTURED, //< Sets whether or not the cursor should be constrained to the source widget and invisible. + //< Internally, this will reset the cursor position after each move event to ensure movement + //< events don't allow the cursor to escape. This can be used for typical camera controls + //< like a dolly or rotation, where mouse movement is important but cursor location is not. + CURSOR_MODE_WRAPPED, //< Flags whether the curser is going to wrap around the soruce widget. + CURSOR_MODE_WRAPPED_X, + CURSOR_MODE_WRAPPED_Y + }; + QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId = 0); ~QtEventToAzInputMapper() = default; @@ -56,8 +67,12 @@ namespace AzToolsFramework //! Internally, this will reset the cursor position after each move event to ensure movement //! events don't allow the cursor to escape. This can be used for typical camera controls //! like a dolly or rotation, where mouse movement is important but cursor location is not. + //! @deprecated Use #SetCursorMode() void SetCursorCaptureEnabled(bool enabled); + //! Set the cursor mode. + void SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode); + void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride); void ClearOverrideCursor(); @@ -176,8 +191,8 @@ namespace AzToolsFramework QWidget* m_sourceWidget; // Flags whether or not Qt events should currently be processed. bool m_enabled = true; - // Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement). - bool m_capturingCursor = false; + // Controls the cursor behavior. + QtEventToAzInputMapper::CursorInputMode m_cursorMode = CURSOR_MODE_NONE; // Flags whether the cursor has been overridden. bool m_overrideCursor = false; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index 505ff70122..cbede827e2 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -331,12 +331,12 @@ namespace AtomToolsFramework void RenderViewportWidget::BeginCursorCapture() { - m_inputChannelMapper->SetCursorCaptureEnabled(true); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_CAPTURED); } void RenderViewportWidget::EndCursorCapture() { - m_inputChannelMapper->SetCursorCaptureEnabled(false); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_NONE); } void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) From fa809a76ca3804e95377c0776a40cf4004da90c8 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Mon, 13 Dec 2021 07:28:28 -0800 Subject: [PATCH 03/37] chore: address changes - updated checkstyle - update enum CursorInputMode - minor refeactor to QtEventToAzInputMapper Signed-off-by: Michael Pollind --- Code/Editor/EditorViewportSettings.cpp | 10 +- Code/Editor/EditorViewportSettings.h | 4 +- .../test_ModularViewportCameraController.cpp | 4 +- .../Input/QtEventToAzInputMapper.cpp | 116 +++++++++--------- .../Input/QtEventToAzInputMapper.h | 14 +-- .../Source/Viewport/RenderViewportWidget.cpp | 4 +- 6 files changed, 74 insertions(+), 78 deletions(-) diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 5711d1c3d3..883eb91863 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -23,7 +23,7 @@ namespace SandboxEditor constexpr AZStd::string_view AngleSizeSetting = "/Amazon/Preferences/Editor/AngleSize"; constexpr AZStd::string_view ShowGridSetting = "/Amazon/Preferences/Editor/ShowGrid"; constexpr AZStd::string_view StickySelectSetting = "/Amazon/Preferences/Editor/StickySelect"; - constexpr AZStd::string_view ManipulatorMouseWrapSetting = "/Amazon/Preferences/Editor/Manipulator/MouseWrapping"; + constexpr AZStd::string_view ViewportMouseWrapSetting = "/Amazon/Preferences/Editor/Manipulator/MouseWrapping"; constexpr AZStd::string_view ManipulatorLineBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/LineBoundWidth"; constexpr AZStd::string_view ManipulatorCircleBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/CircleBoundWidth"; constexpr AZStd::string_view CameraTranslateSpeedSetting = "/Amazon/Preferences/Editor/Camera/TranslateSpeed"; @@ -187,14 +187,14 @@ namespace SandboxEditor AzToolsFramework::SetRegistry(ManipulatorLineBoundWidthSetting, lineBoundWidth); } - bool ManipulatorMouseWrap() + bool ViewportMouseWrapSetting() { - return aznumeric_cast(GetRegistry(ManipulatorMouseWrapSetting, false)); + return aznumeric_cast(GetRegistry(ViewportMouseWrapSetting, false)); } - void SetManipulatorMouseWrap(bool wrapping) + void SetViewportMouseWrapSetting(bool wrapping) { - SetRegistry(ManipulatorMouseWrapSetting, wrapping); + SetRegistry(ViewportMouseWrapSetting, wrapping); } float ManipulatorCircleBoundWidth() diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index 975feff307..a27187bc55 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -57,8 +57,8 @@ namespace SandboxEditor SANDBOX_API float ManipulatorLineBoundWidth(); SANDBOX_API void SetManipulatorLineBoundWidth(float lineBoundWidth); - SANDBOX_API bool ManipulatorMouseWrap(); - SANDBOX_API void SetManipulatorMouseWrap(bool wrapping); + SANDBOX_API bool ViewportMouseWrapSetting(); + SANDBOX_API void SetViewportMouseWrapSetting(bool wrapping); SANDBOX_API float ManipulatorCircleBoundWidth(); SANDBOX_API void SetManipulatorCircleBoundWidth(float circleBoundWidth); diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index f2ee3c79aa..9bfd9d396e 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -47,12 +47,12 @@ namespace UnitTest void ViewportMouseCursorRequestImpl::BeginCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_CAPTURED); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeCaptured); } void ViewportMouseCursorRequestImpl::EndCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_NONE); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeNone); } bool ViewportMouseCursorRequestImpl::IsMouseOver() const diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp index 0b6df58c1a..77f297b98c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp @@ -241,40 +241,32 @@ namespace AzToolsFramework } } - - void QtEventToAzInputMapper::SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode) + void QtEventToAzInputMapper::SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode) { - if(mode != m_cursorMode) + if (mode != m_cursorMode) { m_cursorMode = mode; - switch(m_cursorMode) + switch (m_cursorMode) { - case CURSOR_MODE_CAPTURED: - qApp->setOverrideCursor(Qt::BlankCursor); - m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden); - break; - case CURSOR_MODE_WRAPPED: - qApp->restoreOverrideCursor(); - m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); - break; - case CURSOR_MODE_NONE: - qApp->restoreOverrideCursor(); - m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); - break; + case CursorInputMode::CursorModeCaptured: + qApp->setOverrideCursor(Qt::BlankCursor); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::ConstrainedAndHidden); + break; + case CursorInputMode::CursorModeWrapped: + qApp->restoreOverrideCursor(); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); + break; + case CursorInputMode::CursorModeNone: + qApp->restoreOverrideCursor(); + m_mouseDevice->SetSystemCursorState(AzFramework::SystemCursorState::UnconstrainedAndVisible); + break; } } } void QtEventToAzInputMapper::SetCursorCaptureEnabled(bool enabled) { - if (enabled) - { - SetCursorMode(CURSOR_MODE_CAPTURED); - } - else - { - SetCursorMode(CURSOR_MODE_NONE); - } + SetCursorMode(enabled ? CursorInputMode::CursorModeCaptured : CursorInputMode::CursorModeNone); } bool QtEventToAzInputMapper::eventFilter(QObject* object, QEvent* event) @@ -453,71 +445,75 @@ namespace AzToolsFramework return QPoint{ denormalizedX, denormalizedY }; } - void wrapCursorX(const QRect& rect, QPoint& point) { - if (rect.left() < point.x()) + void wrapCursorX(const QRect& rect, QPoint& point) + { + if (rect.left() < point.x()) { point.setX(rect.right() - 1); } - else if (rect.right() > point.x()) + else if (rect.right() > point.x()) { point.setX(rect.left() + 1); } } - void wrapCursorY(const QRect& rect, QPoint& point) { - if (rect.top() < point.y()) + void wrapCursorY(const QRect& rect, QPoint& point) + { + if (rect.top() < point.y()) { point.setY(rect.bottom() - 1); } - else if (rect.bottom() > point.y()) + else if (rect.bottom() > point.y()) { point.setY(rect.top() + 1); } } - void QtEventToAzInputMapper::HandleMouseMoveEvent(const QPoint& globalCursorPosition) { const QPoint cursorDelta = globalCursorPosition - m_previousGlobalCursorPosition; QScreen* screen = m_sourceWidget->screen(); - const QRect widgetRect(m_sourceWidget->mapToGlobal(QPoint(0,0)), m_sourceWidget->size()); + const QRect widgetRect(m_sourceWidget->mapToGlobal(QPoint(0, 0)), m_sourceWidget->size()); m_mouseDevice->m_cursorPositionData2D->m_normalizedPosition = WidgetPositionToNormalizedPosition(m_sourceWidget->mapFromGlobal(globalCursorPosition)); m_mouseDevice->m_cursorPositionData2D->m_normalizedPositionDelta = WidgetPositionToNormalizedPosition(cursorDelta); - switch(m_cursorMode) + switch (m_cursorMode) { - case CURSOR_MODE_CAPTURED: - AzQtComponents::SetCursorPos(m_previousGlobalCursorPosition); - break; - case CURSOR_MODE_WRAPPED_X: + case CursorInputMode::CursorModeCaptured: + AzQtComponents::SetCursorPos(m_previousGlobalCursorPosition); + break; + case CursorInputMode::CursorModeWrappedX: + case CursorInputMode::CursorModeWrappedY: + case CursorInputMode::CursorModeWrapped: + { QPoint screenPos(globalCursorPosition); - wrapCursorX(widgetRect, screenPos); + switch (m_cursorMode) + { + case CursorInputMode::CursorModeWrappedX: + wrapCursorX(widgetRect, screenPos); + break; + case CursorInputMode::CursorModeWrappedY: + wrapCursorY(widgetRect, screenPos); + break; + case CursorInputMode::CursorModeWrapped: + wrapCursorX(widgetRect, screenPos); + wrapCursorY(widgetRect, screenPos); + break; + default: + // this should never happen + AZ_Assert(false, "Invalid Curosr Mode: %i.", m_cursorMode); + break; + } QCursor::setPos(screen, screenPos); - QPoint screenDelta = globalCursorPosition - screenPos; + const QPoint screenDelta = globalCursorPosition - screenPos; m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; - break; - case CURSOR_MODE_WRAPPED_Y: - QPoint screenPos(globalCursorPosition); - wrapCursorY(widgetRect, screenPos); - QCursor::setPos(screen, screenPos); - QPoint screenDelta = globalCursorPosition - screenPos; - m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; - break; - case CURSOR_MODE_WRAPPED: - QPoint screenPos(globalCursorPosition); - wrapCursorX(widgetRect, screenPos); - wrapCursorY(widgetRect, screenPos); - QCursor::setPos(screen, screenPos); - QPoint screenDelta = globalCursorPosition - screenPos; - m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; - break; - default: - m_previousGlobalCursorPosition = globalCursorPosition; - break; - - + } + break; + default: + AZ_Assert(false, "Invalid Curosr Mode: %i.", m_cursorMode); + break; } ProcessPendingMouseEvents(cursorDelta); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h index 7ba4e21bd4..a6d9fdd753 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h @@ -41,15 +41,15 @@ namespace AzToolsFramework Q_OBJECT public: - enum CursorInputMode { - CURSOR_MODE_NONE, - CURSOR_MODE_CAPTURED, //< Sets whether or not the cursor should be constrained to the source widget and invisible. + enum class CursorInputMode { + CursorModeNone, + CursorModeCaptured, //< Sets whether or not the cursor should be constrained to the source widget and invisible. //< Internally, this will reset the cursor position after each move event to ensure movement //< events don't allow the cursor to escape. This can be used for typical camera controls //< like a dolly or rotation, where mouse movement is important but cursor location is not. - CURSOR_MODE_WRAPPED, //< Flags whether the curser is going to wrap around the soruce widget. - CURSOR_MODE_WRAPPED_X, - CURSOR_MODE_WRAPPED_Y + CursorModeWrapped, //< Flags whether the curser is going to wrap around the soruce widget. + CursorModeWrappedX, + CursorModeWrappedY }; QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId = 0); @@ -192,7 +192,7 @@ namespace AzToolsFramework // Flags whether or not Qt events should currently be processed. bool m_enabled = true; // Controls the cursor behavior. - QtEventToAzInputMapper::CursorInputMode m_cursorMode = CURSOR_MODE_NONE; + QtEventToAzInputMapper::CursorInputMode m_cursorMode = CursorInputMode::CursorModeNone; // Flags whether the cursor has been overridden. bool m_overrideCursor = false; diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index cbede827e2..ef23560d57 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -331,12 +331,12 @@ namespace AtomToolsFramework void RenderViewportWidget::BeginCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_CAPTURED); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeCaptured); } void RenderViewportWidget::EndCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CURSOR_MODE_NONE); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeNone); } void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) From 3a6f877a9f79157424d5a86d05f9f1b7a98d1585 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Tue, 14 Dec 2021 23:05:13 -0800 Subject: [PATCH 04/37] chore: add first test Signed-off-by: Michael Pollind --- .../Input/QtEventToAzInputMapper.cpp | 8 ++--- .../Input/QtEventToAzInputMapperTests.cpp | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp index 77f297b98c..7852a4a5d5 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp @@ -447,11 +447,11 @@ namespace AzToolsFramework void wrapCursorX(const QRect& rect, QPoint& point) { - if (rect.left() < point.x()) + if (point.x() < rect.left()) { point.setX(rect.right() - 1); } - else if (rect.right() > point.x()) + else if (point.x() > rect.right()) { point.setX(rect.left() + 1); } @@ -459,11 +459,11 @@ namespace AzToolsFramework void wrapCursorY(const QRect& rect, QPoint& point) { - if (rect.top() < point.y()) + if (point.y() < rect.top()) { point.setY(rect.bottom() - 1); } - else if (rect.bottom() > point.y()) + else if (point.y() > rect.bottom()) { point.setY(rect.top() + 1); } diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 4c813a4bdd..e645c365e2 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -44,6 +44,10 @@ namespace UnitTest QObject::connect(m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(), [this]([[maybe_unused]] const AzFramework::InputChannel* inputChannel, QEvent* event) { + if(event == nullptr) { + return; + } + const QEvent::Type eventType = event->type(); if (eventType == QEvent::Type::MouseButtonPress || @@ -512,4 +516,32 @@ namespace UnitTest return info.param.m_az.GetName(); } ); + + TEST_F(QtEventToAzInputMapperFixture, MouseWrapMouseViewportQtEventToAzInputMapperFixture) + { + AzFramework::InputChannelNotificationBus::Handler::BusConnect(); + m_captureAzEvents = false; + + + const auto startPos = QPoint(WidgetSize.width() - 2, WidgetSize.height() / 2); + MouseMove(m_rootWidget.get(), startPos, QPoint(0,0)); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorInputMode::CursorModeWrappedX); + + const auto deltaPos = QPoint(200.0f, 0); + const auto expectedPosition = m_rootWidget->mapToGlobal(QPoint(200, WidgetSize.height() / 2)); + const int iterations = 50; + + for(float i = 0; i < iterations; i++) { + MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos()), (deltaPos / iterations)); + } + + QPointF endPosition = QCursor::pos(); + EXPECT_NEAR(endPosition.x(), expectedPosition.x(), 5.0f); + EXPECT_NEAR(endPosition.y(), expectedPosition.y(), 5.0f); + + // cleanup + m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorInputMode::CursorModeNone); + AzFramework::InputChannelNotificationBus::Handler::BusDisconnect(); + } + } // namespace UnitTest From 9e91c1872670c73cd1f3de98de616675ce9da8a5 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Wed, 15 Dec 2021 13:32:24 -0800 Subject: [PATCH 05/37] chore: address changes add test cases Signed-off-by: Michael Pollind --- Code/Editor/EditorViewportSettings.cpp | 2 +- .../Input/QtEventToAzInputMapper.cpp | 14 +- .../Input/QtEventToAzInputMapper.h | 26 +-- .../Input/QtEventToAzInputMapperTests.cpp | 151 +++++++++++++++--- 4 files changed, 153 insertions(+), 40 deletions(-) diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 883eb91863..7108beca5a 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -189,7 +189,7 @@ namespace SandboxEditor bool ViewportMouseWrapSetting() { - return aznumeric_cast(GetRegistry(ViewportMouseWrapSetting, false)); + return GetRegistry(ViewportMouseWrapSetting, false); } void SetViewportMouseWrapSetting(bool wrapping) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp index 7852a4a5d5..edd45a7200 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp @@ -241,7 +241,7 @@ namespace AzToolsFramework } } - void QtEventToAzInputMapper::SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode) + void QtEventToAzInputMapper::SetCursorMode(AzToolsFramework::CursorInputMode mode) { if (mode != m_cursorMode) { @@ -445,7 +445,7 @@ namespace AzToolsFramework return QPoint{ denormalizedX, denormalizedY }; } - void wrapCursorX(const QRect& rect, QPoint& point) + void WrapCursorX(const QRect& rect, QPoint& point) { if (point.x() < rect.left()) { @@ -457,7 +457,7 @@ namespace AzToolsFramework } } - void wrapCursorY(const QRect& rect, QPoint& point) + void WrapCursorY(const QRect& rect, QPoint& point) { if (point.y() < rect.top()) { @@ -492,14 +492,14 @@ namespace AzToolsFramework switch (m_cursorMode) { case CursorInputMode::CursorModeWrappedX: - wrapCursorX(widgetRect, screenPos); + WrapCursorX(widgetRect, screenPos); break; case CursorInputMode::CursorModeWrappedY: - wrapCursorY(widgetRect, screenPos); + WrapCursorY(widgetRect, screenPos); break; case CursorInputMode::CursorModeWrapped: - wrapCursorX(widgetRect, screenPos); - wrapCursorY(widgetRect, screenPos); + WrapCursorX(widgetRect, screenPos); + WrapCursorY(widgetRect, screenPos); break; default: // this should never happen diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h index a6d9fdd753..b84ac99c74 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h @@ -32,6 +32,17 @@ class QWheelEvent; namespace AzToolsFramework { + enum class CursorInputMode { + CursorModeNone, + CursorModeCaptured, //!< Sets whether or not the cursor should be constrained to the source widget and invisible. + //!< Internally, this will reset the cursor position after each move event to ensure movement + //!< events don't allow the cursor to escape. This can be used for typical camera controls + //!< like a dolly or rotation, where mouse movement is important but cursor location is not. + CursorModeWrapped, //!< Flags whether the curser is going to wrap around the soruce widget. + CursorModeWrappedX, + CursorModeWrappedY + }; + //! Maps events from the Qt input system to synthetic InputChannels in AzFramework //! that can be used by AzFramework::ViewportControllers. class QtEventToAzInputMapper final @@ -41,17 +52,6 @@ namespace AzToolsFramework Q_OBJECT public: - enum class CursorInputMode { - CursorModeNone, - CursorModeCaptured, //< Sets whether or not the cursor should be constrained to the source widget and invisible. - //< Internally, this will reset the cursor position after each move event to ensure movement - //< events don't allow the cursor to escape. This can be used for typical camera controls - //< like a dolly or rotation, where mouse movement is important but cursor location is not. - CursorModeWrapped, //< Flags whether the curser is going to wrap around the soruce widget. - CursorModeWrappedX, - CursorModeWrappedY - }; - QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId = 0); ~QtEventToAzInputMapper() = default; @@ -71,7 +71,7 @@ namespace AzToolsFramework void SetCursorCaptureEnabled(bool enabled); //! Set the cursor mode. - void SetCursorMode(QtEventToAzInputMapper::CursorInputMode mode); + void SetCursorMode(AzToolsFramework::CursorInputMode mode); void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride); void ClearOverrideCursor(); @@ -192,7 +192,7 @@ namespace AzToolsFramework // Flags whether or not Qt events should currently be processed. bool m_enabled = true; // Controls the cursor behavior. - QtEventToAzInputMapper::CursorInputMode m_cursorMode = CursorInputMode::CursorModeNone; + AzToolsFramework::CursorInputMode m_cursorMode = AzToolsFramework::CursorInputMode::CursorModeNone; // Flags whether the cursor has been overridden. bool m_overrideCursor = false; diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index e645c365e2..9af445850c 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -44,10 +44,10 @@ namespace UnitTest QObject::connect(m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(), [this]([[maybe_unused]] const AzFramework::InputChannel* inputChannel, QEvent* event) { - if(event == nullptr) { + if(event == nullptr) + { return; } - const QEvent::Type eventType = event->type(); if (eventType == QEvent::Type::MouseButtonPress || @@ -517,31 +517,144 @@ namespace UnitTest } ); - TEST_F(QtEventToAzInputMapperFixture, MouseWrapMouseViewportQtEventToAzInputMapperFixture) + struct MouseMoveParam { + AzToolsFramework::CursorInputMode mode; + int iterations; + QPoint startPos; + QPoint deltaPos; + QPoint expectedPos; + const char* name; + }; + + class MoveMoveWrapParamQtEventToAzInputMapperFixture + : public QtEventToAzInputMapperFixture + , public ::testing::WithParamInterface + { + }; + + TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, MouseMove_NoAzHandlers_VerifyMouseMovmentViewport) + { + // setup + const MouseMoveParam mouseMoveParam = GetParam(); + AzFramework::InputChannelNotificationBus::Handler::BusConnect(); m_captureAzEvents = false; + m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode); + m_rootWidget->move(100, 100); - - const auto startPos = QPoint(WidgetSize.width() - 2, WidgetSize.height() / 2); - MouseMove(m_rootWidget.get(), startPos, QPoint(0,0)); - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorInputMode::CursorModeWrappedX); - const auto deltaPos = QPoint(200.0f, 0); - const auto expectedPosition = m_rootWidget->mapToGlobal(QPoint(200, WidgetSize.height() / 2)); - const int iterations = 50; - - for(float i = 0; i < iterations; i++) { - MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos()), (deltaPos / iterations)); + MouseMove(m_rootWidget.get(), mouseMoveParam.startPos, QPoint(0,0)); + for(float i = 0; i < mouseMoveParam.iterations; i++) { + MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos()), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); } - - QPointF endPosition = QCursor::pos(); - EXPECT_NEAR(endPosition.x(), expectedPosition.x(), 5.0f); - EXPECT_NEAR(endPosition.y(), expectedPosition.y(), 5.0f); - + + QPointF endPosition = m_rootWidget->mapFromGlobal(QCursor::pos()); + EXPECT_NEAR(endPosition.x(), mouseMoveParam.expectedPos.x(), 1.0f); + EXPECT_NEAR(endPosition.y(), mouseMoveParam.expectedPos.y(), 1.0f); + // cleanup - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorInputMode::CursorModeNone); + m_rootWidget->move(0, 0); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone); AzFramework::InputChannelNotificationBus::Handler::BusDisconnect(); } + INSTANTIATE_TEST_CASE_P(All, MoveMoveWrapParamQtEventToAzInputMapperFixture, + testing::Values( + // verify CursorModeWrappedX wrapping + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(40, 0), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrappedX_Test_Right" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, + 40, + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(-40, 0), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrappedX_Test_Left" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(0, -40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, -20.0f), + "CursorModeWrappedX_Test_Top" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(0, 40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() + 20), + "CursorModeWrappedX_Test_Bottom" + }, + + // verify CursorModeWrappedY wrapping + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(40, 0), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() + 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrappedY_Test_Right" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, + 40, + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(-40, 0), + QPoint(-20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrappedY_Test_Left" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(0, -40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20.0f), + "CursorModeWrappedY_Test_Top" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(0, 40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20), + "CursorModeWrappedY_Test_Bottom" + }, + + // verify CursorModeWrapped wrapping + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(40, 0), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrapped_Test_Right" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, + 40, + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(-40, 0), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + "CursorModeWrapped_Test_Left" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(0, -40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20.0f), + "CursorModeWrapped_Test_Top" + }, + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(0, 40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20), + "CursorModeWrapped_Test_Bottom" + } + ), + [](const ::testing::TestParamInfo& info) + { + return info.param.name; + } + ); + } // namespace UnitTest From 1654ff052041567778624fd7078164533bc37f42 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Thu, 16 Dec 2021 22:37:31 -0800 Subject: [PATCH 06/37] chore: correct test case MouseMove_NoAzHandlers_VerifyMouseMovementViewport Signed-off-by: Michael Pollind --- .../Input/QtEventToAzInputMapper.cpp | 3 ++ .../Input/QtEventToAzInputMapper.h | 4 +- .../Input/QtEventToAzInputMapperTests.cpp | 43 ++++++++++++++++--- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp index edd45a7200..b660e87b56 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.cpp @@ -511,6 +511,9 @@ namespace AzToolsFramework m_previousGlobalCursorPosition = globalCursorPosition - screenDelta; } break; + case CursorInputMode::CursorModeNone: + m_previousGlobalCursorPosition = globalCursorPosition; + break; default: AZ_Assert(false, "Invalid Curosr Mode: %i.", m_cursorMode); break; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h index b84ac99c74..67ef195363 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Input/QtEventToAzInputMapper.h @@ -39,8 +39,8 @@ namespace AzToolsFramework //!< events don't allow the cursor to escape. This can be used for typical camera controls //!< like a dolly or rotation, where mouse movement is important but cursor location is not. CursorModeWrapped, //!< Flags whether the curser is going to wrap around the soruce widget. - CursorModeWrappedX, - CursorModeWrappedY + CursorModeWrappedX, //!< Flags whether the curser is going to wrap around the soruce widget only on the left and right side. + CursorModeWrappedY //!< Flags whether the curser is going to wrap around the soruce widget only on the top and bottom side. }; //! Maps events from the Qt input system to synthetic InputChannels in AzFramework diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 9af445850c..0282812be7 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -533,23 +533,38 @@ namespace UnitTest { }; - TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, MouseMove_NoAzHandlers_VerifyMouseMovmentViewport) + TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, MouseMove_NoAzHandlers_VerifyMouseMovementViewport) { + //TODO: mouseMove is bugged mapToGlobal is called twice + auto mouseMoveFix = [](QWidget* wid, QPoint globalPos, QPoint deltaPos) + { + QPoint globalPosition = globalPos + deltaPos; + QPoint localPosition = wid->mapFromGlobal(globalPosition); + QTest::mouseMove(wid, localPosition); + QMouseEvent mouseMoveEvent(QEvent::MouseMove, localPosition, globalPosition, Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QApplication::sendEvent(wid, &mouseMoveEvent); + }; + // setup const MouseMoveParam mouseMoveParam = GetParam(); AzFramework::InputChannelNotificationBus::Handler::BusConnect(); m_captureAzEvents = false; - m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode); + m_rootWidget->move(100, 100); + mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); + // given + m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode); - MouseMove(m_rootWidget.get(), mouseMoveParam.startPos, QPoint(0,0)); - for(float i = 0; i < mouseMoveParam.iterations; i++) { - MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos()), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); + mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); + for (float i = 0; i < mouseMoveParam.iterations; i++) + { + mouseMoveFix(m_rootWidget.get(), QCursor::pos(), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); } - QPointF endPosition = m_rootWidget->mapFromGlobal(QCursor::pos()); + // validate + QPoint endPosition = m_rootWidget->mapFromGlobal(QCursor::pos()); EXPECT_NEAR(endPosition.x(), mouseMoveParam.expectedPos.x(), 1.0f); EXPECT_NEAR(endPosition.y(), mouseMoveParam.expectedPos.y(), 1.0f); @@ -649,6 +664,22 @@ namespace UnitTest QPoint(0, 40), QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20), "CursorModeWrapped_Test_Bottom" + }, + // verify CursorModeCaptured + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeCaptured, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), + QPoint(0, 40), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), + "CursorModeCaptured" + }, + // verify CursorModeNone + MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeNone, + 40, + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), + QPoint(40.0f, 0), + QPoint((QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f) + 40.0f, (QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f)), + "CursorModeNone" } ), [](const ::testing::TestParamInfo& info) From 95c0c83642b2496f96418745b6c245ee50cf87b9 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sat, 18 Dec 2021 18:29:39 -0800 Subject: [PATCH 07/37] chore: remove unused setting and fixed compiling errors Signed-off-by: Michael Pollind --- Code/Editor/EditorViewportSettings.cpp | 11 ----------- Code/Editor/EditorViewportSettings.h | 3 --- .../Tests/test_ModularViewportCameraController.cpp | 4 ++-- .../Code/Source/Viewport/RenderViewportWidget.cpp | 4 ++-- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/Code/Editor/EditorViewportSettings.cpp b/Code/Editor/EditorViewportSettings.cpp index 7108beca5a..e06b9696e1 100644 --- a/Code/Editor/EditorViewportSettings.cpp +++ b/Code/Editor/EditorViewportSettings.cpp @@ -23,7 +23,6 @@ namespace SandboxEditor constexpr AZStd::string_view AngleSizeSetting = "/Amazon/Preferences/Editor/AngleSize"; constexpr AZStd::string_view ShowGridSetting = "/Amazon/Preferences/Editor/ShowGrid"; constexpr AZStd::string_view StickySelectSetting = "/Amazon/Preferences/Editor/StickySelect"; - constexpr AZStd::string_view ViewportMouseWrapSetting = "/Amazon/Preferences/Editor/Manipulator/MouseWrapping"; constexpr AZStd::string_view ManipulatorLineBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/LineBoundWidth"; constexpr AZStd::string_view ManipulatorCircleBoundWidthSetting = "/Amazon/Preferences/Editor/Manipulator/CircleBoundWidth"; constexpr AZStd::string_view CameraTranslateSpeedSetting = "/Amazon/Preferences/Editor/Camera/TranslateSpeed"; @@ -187,16 +186,6 @@ namespace SandboxEditor AzToolsFramework::SetRegistry(ManipulatorLineBoundWidthSetting, lineBoundWidth); } - bool ViewportMouseWrapSetting() - { - return GetRegistry(ViewportMouseWrapSetting, false); - } - - void SetViewportMouseWrapSetting(bool wrapping) - { - SetRegistry(ViewportMouseWrapSetting, wrapping); - } - float ManipulatorCircleBoundWidth() { return aznumeric_cast(AzToolsFramework::GetRegistry(ManipulatorCircleBoundWidthSetting, 0.1)); diff --git a/Code/Editor/EditorViewportSettings.h b/Code/Editor/EditorViewportSettings.h index a27187bc55..fe1253ed0c 100644 --- a/Code/Editor/EditorViewportSettings.h +++ b/Code/Editor/EditorViewportSettings.h @@ -57,9 +57,6 @@ namespace SandboxEditor SANDBOX_API float ManipulatorLineBoundWidth(); SANDBOX_API void SetManipulatorLineBoundWidth(float lineBoundWidth); - SANDBOX_API bool ViewportMouseWrapSetting(); - SANDBOX_API void SetViewportMouseWrapSetting(bool wrapping); - SANDBOX_API float ManipulatorCircleBoundWidth(); SANDBOX_API void SetManipulatorCircleBoundWidth(float circleBoundWidth); diff --git a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp index 9bfd9d396e..5009c626e2 100644 --- a/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp +++ b/Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp @@ -47,12 +47,12 @@ namespace UnitTest void ViewportMouseCursorRequestImpl::BeginCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeCaptured); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeCaptured); } void ViewportMouseCursorRequestImpl::EndCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeNone); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone); } bool ViewportMouseCursorRequestImpl::IsMouseOver() const diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp index ef23560d57..d6871a8795 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Viewport/RenderViewportWidget.cpp @@ -331,12 +331,12 @@ namespace AtomToolsFramework void RenderViewportWidget::BeginCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeCaptured); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeCaptured); } void RenderViewportWidget::EndCursorCapture() { - m_inputChannelMapper->SetCursorMode(AzToolsFramework::QtEventToAzInputMapper::CursorModeNone); + m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone); } void RenderViewportWidget::SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) From ed39c784c304c0cf9a374b297f3521f7f0787fc2 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sun, 19 Dec 2021 18:22:14 -0800 Subject: [PATCH 08/37] chore: add accumulated test for mouse position Signed-off-by: Michael Pollind --- .../Tests/Input/QtEventToAzInputMapperTests.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 0282812be7..5047c19044 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -100,6 +100,11 @@ namespace UnitTest m_azChannelEvents.push_back(AzEventInfo(inputChannel)); hasBeenConsumed = m_captureAzEvents; } + else if (inputChannelId == AzFramework::InputDeviceMouse::SystemCursorPosition) + { + m_azCursorPositions.push_back(*inputChannel.GetCustomData()); + hasBeenConsumed = m_captureAzEvents; + } } else if (AzFramework::InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId)) { @@ -165,6 +170,7 @@ namespace UnitTest AZStd::vector m_signalEvents; AZStd::vector m_azChannelEvents; AZStd::vector m_azTextEvents; + AZStd::vector m_azCursorPositions; bool m_captureAzEvents{ false }; bool m_captureTextEvents{ false }; @@ -549,13 +555,14 @@ namespace UnitTest const MouseMoveParam mouseMoveParam = GetParam(); AzFramework::InputChannelNotificationBus::Handler::BusConnect(); - m_captureAzEvents = false; + m_captureAzEvents = true; m_rootWidget->move(100, 100); mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); // given m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode); + m_azCursorPositions.clear(); mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); for (float i = 0; i < mouseMoveParam.iterations; i++) @@ -563,11 +570,19 @@ namespace UnitTest mouseMoveFix(m_rootWidget.get(), QCursor::pos(), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); } + AZ::Vector2 accumalatedPosition(0.0f,0.0f); + for(const auto& pos: m_azCursorPositions) { + accumalatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(WidgetSize.width(), WidgetSize.height())); + } + // validate QPoint endPosition = m_rootWidget->mapFromGlobal(QCursor::pos()); EXPECT_NEAR(endPosition.x(), mouseMoveParam.expectedPos.x(), 1.0f); EXPECT_NEAR(endPosition.y(), mouseMoveParam.expectedPos.y(), 1.0f); + EXPECT_NEAR(accumalatedPosition.GetX(), mouseMoveParam.deltaPos.x(), 1.0f); + EXPECT_NEAR(accumalatedPosition.GetY(), mouseMoveParam.deltaPos.y(), 1.0f); + // cleanup m_rootWidget->move(0, 0); m_inputChannelMapper->SetCursorMode(AzToolsFramework::CursorInputMode::CursorModeNone); From 832c525f67a0ea24ad004066c0d556c8c449a9af Mon Sep 17 00:00:00 2001 From: nemerle <96597+nemerle@users.noreply.github.com> Date: Sat, 8 Jan 2022 23:22:04 +0100 Subject: [PATCH 09/37] Fix missing include file Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> --- .../Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h index 838cdd2256..60417adf36 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Shader/ShaderVariantAsyncLoader.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace AZ { From c65a903c7dc8ae5ce441df130f26e735534dd6bd Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sat, 8 Jan 2022 21:01:29 -0800 Subject: [PATCH 10/37] chore: fix unit test Signed-off-by: Michael Pollind --- .../Input/QtEventToAzInputMapperTests.cpp | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 5047c19044..d7ab896462 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -541,15 +541,6 @@ namespace UnitTest TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, MouseMove_NoAzHandlers_VerifyMouseMovementViewport) { - //TODO: mouseMove is bugged mapToGlobal is called twice - auto mouseMoveFix = [](QWidget* wid, QPoint globalPos, QPoint deltaPos) - { - QPoint globalPosition = globalPos + deltaPos; - QPoint localPosition = wid->mapFromGlobal(globalPosition); - QTest::mouseMove(wid, localPosition); - QMouseEvent mouseMoveEvent(QEvent::MouseMove, localPosition, globalPosition, Qt::NoButton, Qt::NoButton, Qt::NoModifier); - QApplication::sendEvent(wid, &mouseMoveEvent); - }; // setup const MouseMoveParam mouseMoveParam = GetParam(); @@ -558,30 +549,29 @@ namespace UnitTest m_captureAzEvents = true; m_rootWidget->move(100, 100); - mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); + QScreen* screen = m_rootWidget->screen(); + MouseMove(m_rootWidget.get(), mouseMoveParam.startPos, QPoint(0, 0)); // given m_inputChannelMapper->SetCursorMode(mouseMoveParam.mode); m_azCursorPositions.clear(); - - mouseMoveFix(m_rootWidget.get(), m_rootWidget->mapToGlobal(mouseMoveParam.startPos), QPoint(0, 0)); for (float i = 0; i < mouseMoveParam.iterations; i++) { - mouseMoveFix(m_rootWidget.get(), QCursor::pos(), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); + MouseMove(m_rootWidget.get(), m_rootWidget->mapFromGlobal(QCursor::pos(screen)), (mouseMoveParam.deltaPos / mouseMoveParam.iterations)); } - AZ::Vector2 accumalatedPosition(0.0f,0.0f); + AZ::Vector2 accumulatedPosition(0.0f,0.0f); for(const auto& pos: m_azCursorPositions) { - accumalatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(WidgetSize.width(), WidgetSize.height())); + accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(WidgetSize.width(), WidgetSize.height())); } // validate - QPoint endPosition = m_rootWidget->mapFromGlobal(QCursor::pos()); + const QPoint endPosition = m_rootWidget->mapFromGlobal(QCursor::pos(screen)); EXPECT_NEAR(endPosition.x(), mouseMoveParam.expectedPos.x(), 1.0f); EXPECT_NEAR(endPosition.y(), mouseMoveParam.expectedPos.y(), 1.0f); - EXPECT_NEAR(accumalatedPosition.GetX(), mouseMoveParam.deltaPos.x(), 1.0f); - EXPECT_NEAR(accumalatedPosition.GetY(), mouseMoveParam.deltaPos.y(), 1.0f); + EXPECT_NEAR(accumulatedPosition.GetX(), mouseMoveParam.deltaPos.x(), 1.0f); + EXPECT_NEAR(accumulatedPosition.GetY(), mouseMoveParam.deltaPos.y(), 1.0f); // cleanup m_rootWidget->move(0, 0); From 3cac520280a586e38292f6906947b143ac9147e7 Mon Sep 17 00:00:00 2001 From: nemerle <96597+nemerle@users.noreply.github.com> Date: Tue, 11 Jan 2022 00:47:00 +0100 Subject: [PATCH 11/37] Fix non-unity windows build Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> --- Code/Framework/AzCore/Tests/TestCatalog.h | 1 + Gems/Blast/Code/Include/Blast/BlastSystemBus.h | 1 + Gems/Blast/Code/Source/Family/BlastFamilyImpl.cpp | 1 + .../Code/Source/Components/TerrainPhysicsColliderComponent.cpp | 2 ++ 4 files changed, 5 insertions(+) diff --git a/Code/Framework/AzCore/Tests/TestCatalog.h b/Code/Framework/AzCore/Tests/TestCatalog.h index 9a1fb9b5e6..bbcd952f78 100644 --- a/Code/Framework/AzCore/Tests/TestCatalog.h +++ b/Code/Framework/AzCore/Tests/TestCatalog.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace UnitTest { diff --git a/Gems/Blast/Code/Include/Blast/BlastSystemBus.h b/Gems/Blast/Code/Include/Blast/BlastSystemBus.h index 95ffd75057..bda684181a 100644 --- a/Gems/Blast/Code/Include/Blast/BlastSystemBus.h +++ b/Gems/Blast/Code/Include/Blast/BlastSystemBus.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include diff --git a/Gems/Blast/Code/Source/Family/BlastFamilyImpl.cpp b/Gems/Blast/Code/Source/Family/BlastFamilyImpl.cpp index 56c785b310..70e7c28bac 100644 --- a/Gems/Blast/Code/Source/Family/BlastFamilyImpl.cpp +++ b/Gems/Blast/Code/Source/Family/BlastFamilyImpl.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp index 8c2c0e80b6..fccbedd8fa 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp @@ -6,12 +6,14 @@ * */ + #include #include #include #include #include +#include #include #include #include From 7b6ce50fe87c8306bc6b3ab1b65e4ddbdaf463be Mon Sep 17 00:00:00 2001 From: nemerle <96597+nemerle@users.noreply.github.com> Date: Tue, 11 Jan 2022 00:52:21 +0100 Subject: [PATCH 12/37] Remove un-needed `Requests` namespace use Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> --- Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp index 90014578ea..13c01b2efc 100644 --- a/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Streamer/FileRequest.cpp @@ -139,7 +139,7 @@ namespace AZ::IO::Requests { } - Requests::ReportData::ReportData(ReportType reportType) + ReportData::ReportData(ReportType reportType) : m_reportType(reportType) { } From 2e19b703ef00e4f1bc94a904901c83f2e6fa3e3e Mon Sep 17 00:00:00 2001 From: nemerle <96597+nemerle@users.noreply.github.com> Date: Wed, 12 Jan 2022 01:37:02 +0100 Subject: [PATCH 13/37] fix release build Signed-off-by: nemerle <96597+nemerle@users.noreply.github.com> --- Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp index 45a95a71d7..f799e8e69f 100644 --- a/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp +++ b/Gems/AudioSystem/Code/Source/Engine/ATLEntities.cpp @@ -286,7 +286,9 @@ namespace Audio return sResult; } - CATLAudioFileEntry::CATLAudioFileEntry(const char * const filePath, IATLAudioFileEntryData * const implData) + +#endif // !AUDIO_RELEASE + CATLAudioFileEntry::CATLAudioFileEntry(const char* const filePath, IATLAudioFileEntryData* const implData) : m_filePath(filePath) , m_fileSize(0) , m_useCount(0) @@ -299,6 +301,4 @@ namespace Audio } CATLAudioFileEntry::~CATLAudioFileEntry() = default; - -#endif // !AUDIO_RELEASE } // namespace Audio From fabb2b186c4d3f0b9cd2aa49a1233977fa796e12 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Wed, 12 Jan 2022 20:52:57 -0800 Subject: [PATCH 14/37] chore: disable mouse move test Signed-off-by: Michael Pollind --- .../Tests/Input/QtEventToAzInputMapperTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index d7ab896462..31193f6f05 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -539,7 +539,7 @@ namespace UnitTest { }; - TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, MouseMove_NoAzHandlers_VerifyMouseMovementViewport) + TEST_P(MoveMoveWrapParamQtEventToAzInputMapperFixture, DISABLED_MouseMove_NoAzHandlers_VerifyMouseMovementViewport) { // setup From c31e3c020898816b1a2eee355af202e7c952c56c Mon Sep 17 00:00:00 2001 From: Sergey Pereslavtsev Date: Thu, 20 Jan 2022 19:36:33 +0000 Subject: [PATCH 15/37] LYN-7640 Terrain Physics Materials hooked up all the way down to PhysX Signed-off-by: Sergey Pereslavtsev --- .../EditorHeightfieldColliderComponent.cpp | 3 + .../Source/HeightfieldColliderComponent.cpp | 4 + Gems/PhysX/Code/Source/Utils.cpp | 132 ++++++----- Gems/PhysX/Code/Source/Utils.h | 7 + ...ditorHeightfieldColliderComponentTests.cpp | 208 ++++++++++++++++-- Gems/PhysX/Code/Tests/EditorTestUtilities.cpp | 18 ++ Gems/PhysX/Code/Tests/EditorTestUtilities.h | 3 + 7 files changed, 302 insertions(+), 73 deletions(-) diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp index fb0a38fa18..c6def5f0ab 100644 --- a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp @@ -189,6 +189,9 @@ namespace PhysX configuration.m_entityId = GetEntityId(); configuration.m_debugName = GetEntity()->GetName(); + // Update material selection from the mapping + Utils::SetMaterialsFromHeightfieldProvider(GetEntityId(), m_colliderConfig.m_materialSelection); + AzPhysics::ShapeColliderPairList colliderShapePairs; colliderShapePairs.emplace_back(AZStd::make_shared(m_colliderConfig), m_shapeConfig); configuration.m_colliderAndShapeData = colliderShapePairs; diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp index c1fa09f21f..b219253b6f 100644 --- a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp @@ -140,6 +140,10 @@ namespace PhysX Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); configuration = Utils::CreateHeightfieldShapeConfiguration(GetEntityId()); + + // Update material selection from the mapping + Physics::ColliderConfiguration* colliderConfig = m_shapeConfig.first.get(); + Utils::SetMaterialsFromHeightfieldProvider(GetEntityId(), colliderConfig->m_materialSelection); } void HeightfieldColliderComponent::RefreshHeightfield() diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index fecea57d9c..a988f6791c 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -66,6 +66,67 @@ namespace PhysX } } + AZStd::pair GetPhysXMaterialIndicesFromHeightfieldSamples( + const AZStd::vector& samples, + const int32_t row, const int32_t col, + const int32_t numRows, const int32_t numCols) + { + + uint8_t materialIndex0 = 0; + uint8_t materialIndex1 = 0; + + const bool lastRowIndex = (row == (numRows - 1)); + const bool lastColumnIndex = (col == (numCols - 1)); + + // In PhysX, the material indices refer to the quad down and to the right of the sample. + // If we're in the last row or last column, there aren't any quads down or to the right, + // so just clear these out. + + if (!lastRowIndex && !lastColumnIndex) + { + auto GetIndex = [numCols](int32_t row, int32_t col) + { + return (row * numCols) + col; + }; + + // Our source data is providing one material index per vertex, but PhysX wants one material index + // per triangle. The heuristic that we'll go with for selecting the material index is to choose + // the material for the vertex that's not on the diagonal of each triangle. + // Ex: A *---* B + // | / | For this, we'll use A for index0 and D for index1. + // C *---* D + // + // Ex: A *---* B + // | \ | For this, we'll use C for index0 and B for index1. + // C *---* D + // + // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this + // causes incorrect or unpredictable physics material mappings. + + const Physics::HeightMaterialPoint& currentSample = samples[GetIndex(row, col)]; + + switch (currentSample.m_quadMeshType) + { + case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: + materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; + materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: + materialIndex0 = currentSample.m_materialIndex; + materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::Hole: + materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; + materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; + break; + default: + AZ_Assert(false, "Unhandled case in GetPhysXMaterialIndicesFromHeightfieldSamples"); + break; + } + } + return { materialIndex0, materialIndex1 }; + } + void CreatePxGeometryFromHeightfield( Physics::HeightfieldShapeConfiguration& heightfieldConfig, physx::PxGeometryHolder& pxGeometry) { @@ -116,12 +177,8 @@ namespace PhysX for (int32_t row = 0; row < numRows; row++) { - const bool lastRowIndex = (row == (numRows - 1)); - for (int32_t col = 0; col < numCols; col++) { - const bool lastColumnIndex = (col == (numCols - 1)); - auto GetIndex = [numCols](int32_t row, int32_t col) { return (row * numCols) + col; @@ -134,52 +191,15 @@ namespace PhysX AZ_Assert(currentSample.m_materialIndex < physxMaximumMaterialIndex, "MaterialIndex must be less than 128"); currentPhysxSample.height = azlossy_cast( AZ::GetClamp(currentSample.m_height, minHeightBounds, maxHeightBounds) * scaleFactor); - if (lastRowIndex || lastColumnIndex) - { - // In PhysX, the material indices refer to the quad down and to the right of the sample. - // If we're in the last row or last column, there aren't any quads down or to the right, - // so just clear these out. - currentPhysxSample.materialIndex0 = 0; - currentPhysxSample.materialIndex1 = 0; - } - else - { - // Our source data is providing one material index per vertex, but PhysX wants one material index - // per triangle. The heuristic that we'll go with for selecting the material index is to choose - // the material for the vertex that's not on the diagonal of each triangle. - // Ex: A *---* B - // | / | For this, we'll use A for index0 and D for index1. - // C *---* D - // - // Ex: A *---* B - // | \ | For this, we'll use C for index0 and B for index1. - // C *---* D - // - // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this - // causes incorrect or unpredictable physics material mappings. - switch (currentSample.m_quadMeshType) - { - case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: - currentPhysxSample.materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; - currentPhysxSample.materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; - // Set the tesselation flag to say that we need to go from UL to BR - currentPhysxSample.materialIndex0.setBit(); - break; - case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: - currentPhysxSample.materialIndex0 = currentSample.m_materialIndex; - currentPhysxSample.materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; - break; - case Physics::QuadMeshType::Hole: - currentPhysxSample.materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; - currentPhysxSample.materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; - break; - default: - AZ_Warning("PhysX Heightfield", false, "Unhandled case in CreatePxGeometryFromConfig"); - currentPhysxSample.materialIndex0 = 0; - currentPhysxSample.materialIndex1 = 0; - break; - } + auto [materialIndex0, materialIndex1] = GetPhysXMaterialIndicesFromHeightfieldSamples(samples, row, col, numRows, numCols); + currentPhysxSample.materialIndex0 = materialIndex0; + currentPhysxSample.materialIndex1 = materialIndex1; + + if (currentSample.m_quadMeshType == Physics::QuadMeshType::SubdivideUpperLeftToBottomRight) + { + // Set the tesselation flag to say that we need to go from UL to BR + currentPhysxSample.setTessFlag(); } } } @@ -1562,6 +1582,20 @@ namespace PhysX return configuration; } + + void SetMaterialsFromHeightfieldProvider(const AZ::EntityId& heightfieldProviderId, Physics::MaterialSelection& materialSelection) + { + AZStd::vector materialList; + Physics::HeightfieldProviderRequestsBus::EventResult( + materialList, heightfieldProviderId, &Physics::HeightfieldProviderRequestsBus::Events::GetMaterialList); + + materialSelection.SetMaterialSlots(Physics::MaterialSelection::SlotsArray(materialList.size(), "")); + + for (int i = 0; i < materialList.size(); ++i) + { + materialSelection.SetMaterialId(materialList[i], i); + } + } } // namespace Utils namespace ReflectionUtils diff --git a/Gems/PhysX/Code/Source/Utils.h b/Gems/PhysX/Code/Source/Utils.h index 525db03315..5244926aa2 100644 --- a/Gems/PhysX/Code/Source/Utils.h +++ b/Gems/PhysX/Code/Source/Utils.h @@ -188,8 +188,15 @@ namespace PhysX //! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged. const AZ::Vector3& Sanitize(const AZ::Vector3& input, const AZ::Vector3& defaultValue = AZ::Vector3::CreateZero()); + AZStd::pair GetPhysXMaterialIndicesFromHeightfieldSamples( + const AZStd::vector& samples, + const int32_t row, const int32_t col, + const int32_t numRows, const int32_t numCols); + Physics::HeightfieldShapeConfiguration CreateHeightfieldShapeConfiguration(AZ::EntityId entityId); + void SetMaterialsFromHeightfieldProvider(const AZ::EntityId& heightfieldProviderId, Physics::MaterialSelection& materialSelection); + namespace Geometry { using PointList = AZStd::vector; diff --git a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp index ef112d8623..9bdc63e60d 100644 --- a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp @@ -19,6 +19,7 @@ #include #include #include +#include using ::testing::NiceMock; using ::testing::Return; @@ -27,18 +28,28 @@ namespace PhysXEditorTests { AZStd::vector GetSamples() { - AZStd::vector samples{ { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 2.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 1.5f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 0.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, - { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight } }; + AZStd::vector samples{ { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, + { 2.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, + { 1.5f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 }, + { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, + { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 0 }, + { 0.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 1 }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight, 2 } }; return samples; } + AZStd::vector GetMaterialList() + { + AZStd::vector materials{ + {Physics::MaterialId::FromUUID("{EC976D51-2C26-4C1E-BBF2-75BAAAFA162C}")}, + {Physics::MaterialId::FromUUID("{B9836F51-A235-4781-95E3-A6302BEE9EFF}")}, + {Physics::MaterialId::FromUUID("{7E060707-BB03-47EB-B046-4503C7145B6E}")} + }; + return materials; + } + EntityPtr SetupHeightfieldComponent() { // create an editor entity with a shape collider component and a box shape component @@ -78,6 +89,7 @@ namespace PhysXEditorTests x = -3.0f; y = 3.0f; }); + ON_CALL(mockShapeRequests, GetMaterialList).WillByDefault(Return(GetMaterialList())); } EntityPtr TestCreateActiveGameEntityFromEditorEntity(AZ::Entity* editorEntity) @@ -89,6 +101,89 @@ namespace PhysXEditorTests return gameEntity; } + class PhysXEditorHeightfieldFixture : public PhysXEditorFixture + { + public: + void SetUp() override + { + PhysXEditorFixture::SetUp(); + PopulateDefaultMaterialLibrary(); + + m_editorEntity = SetupHeightfieldComponent(); + m_editorMockShapeRequests = AZStd::make_unique>(m_editorEntity->GetId()); + SetupMockMethods(*m_editorMockShapeRequests.get()); + m_editorEntity->Activate(); + + m_gameEntity = TestCreateActiveGameEntityFromEditorEntity(m_editorEntity.get()); + m_gameMockShapeRequests = AZStd::make_unique>(m_gameEntity->GetId()); + SetupMockMethods(*m_gameMockShapeRequests.get()); + m_gameEntity->Activate(); + } + + void TearDown() override + { + CleanupHeightfieldComponent(); + + m_editorEntity = nullptr; + m_gameEntity = nullptr; + m_editorMockShapeRequests = nullptr; + m_gameMockShapeRequests = nullptr; + + PhysXEditorFixture::TearDown(); + } + + void PopulateDefaultMaterialLibrary() + { + AZ::Data::AssetId assetId = AZ::Data::AssetId(AZ::Uuid::Create()); + + // Create an asset out of our Script Event + Physics::MaterialLibraryAsset* matLibAsset = aznew Physics::MaterialLibraryAsset; + { + AZStd::vector matIds = GetMaterialList(); + + for (Physics::MaterialId matId : matIds) + { + Physics::MaterialFromAssetConfiguration matConfig; + matConfig.m_id = matId; + matConfig.m_configuration.m_surfaceType = matId.GetUuid().ToString(); + matLibAsset->AddMaterialData(matConfig); + } + } + + // Note: There is no interface to simply update material library asset. It has to go via updating the entire configuration which causes assets reloading. + // It makes sense as a safety mechanism in the Editor but makes it harder to write tests. + // Hence have to work around it via const_cast here to be able to simply set the generated asset into configuration. + AzPhysics::SystemConfiguration* sysConfig = const_cast(AZ::Interface::Get()->GetConfiguration()); + + AZ::Data::Asset assetData(assetId, matLibAsset, AZ::Data::AssetLoadBehavior::Default); + sysConfig->m_materialLibraryAsset = assetData; + } + + Physics::Material* GetMaterialFromRaycast(float x, float y) + { + AzPhysics::RayCastRequest request; + request.m_start = AZ::Vector3(x, y, 5.0f); + request.m_direction = AZ::Vector3(0.0f, 0.0f, -1.0f); + request.m_distance = 10.0f; + + //query the scene + auto* sceneInterface = AZ::Interface::Get(); + AzPhysics::SceneQueryHits result = sceneInterface->QueryScene(m_defaultSceneHandle, &request); + EXPECT_EQ(result.m_hits.size(), 1); + + if (result) + { + return result.m_hits[0].m_material; + } + + return nullptr; + }; + + EntityPtr m_editorEntity; + EntityPtr m_gameEntity; + AZStd::unique_ptr> m_editorMockShapeRequests; + AZStd::unique_ptr> m_gameMockShapeRequests; + }; TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentDependenciesSatisfiedEntityIsValid) { @@ -149,21 +244,13 @@ namespace PhysXEditorTests CleanupHeightfieldComponent(); } - TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithAABoxCorrectRuntimeGeometry) + TEST_F(PhysXEditorHeightfieldFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithAABoxCorrectRuntimeGeometry) { - EntityPtr editorEntity = SetupHeightfieldComponent(); - NiceMock mockShapeRequests(editorEntity->GetId()); - SetupMockMethods(mockShapeRequests); - editorEntity->Activate(); - - EntityPtr gameEntity = TestCreateActiveGameEntityFromEditorEntity(editorEntity.get()); - NiceMock mockShapeRequests2(gameEntity->GetId()); - SetupMockMethods(mockShapeRequests2); - gameEntity->Activate(); + AZ::EntityId gameEntityId = m_gameEntity->GetId(); AzPhysics::SimulatedBody* staticBody = nullptr; AzPhysics::SimulatedBodyComponentRequestsBus::EventResult( - staticBody, gameEntity->GetId(), &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); + staticBody, gameEntityId, &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); const auto* pxRigidStatic = static_cast(staticBody->GetNativePointer()); PHYSX_SCENE_READ_LOCK(pxRigidStatic->getScene()); @@ -183,7 +270,7 @@ namespace PhysXEditorTests int32_t numRows{ 0 }; int32_t numColumns{ 0 }; Physics::HeightfieldProviderRequestsBus::Event( - gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); EXPECT_EQ(numColumns, heightfield->getNbColumns()); EXPECT_EQ(numRows, heightfield->getNbRows()); @@ -194,12 +281,12 @@ namespace PhysXEditorTests float minHeightBounds{ 0.0f }; float maxHeightBounds{ 0.0f }; Physics::HeightfieldProviderRequestsBus::Event( - gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, + gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds); AZStd::vector samples; Physics::HeightfieldProviderRequestsBus::EventResult( - samples, gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + samples, gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; @@ -208,7 +295,80 @@ namespace PhysXEditorTests EXPECT_EQ(samplePhysX.height, azlossy_cast(samplePhysics.m_height * scaleFactor)); } } - CleanupHeightfieldComponent(); + } + + TEST_F(PhysXEditorHeightfieldFixture, EditorHeightfieldColliderComponentHeightfieldColliderCorrectMaterials) + { + AZ::EntityId gameEntityId = m_gameEntity->GetId(); + + int32_t numRows{ 0 }; + int32_t numColumns{ 0 }; + Physics::HeightfieldProviderRequestsBus::Event( + gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + + EXPECT_EQ(numRows, 3); + EXPECT_EQ(numColumns, 3); + + AZStd::vector samples; + Physics::HeightfieldProviderRequestsBus::EventResult( + samples, gameEntityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + + AzPhysics::SimulatedBody* staticBody = nullptr; + AzPhysics::SimulatedBodyComponentRequestsBus::EventResult( + staticBody, gameEntityId, &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); + + const auto* pxRigidStatic = static_cast(staticBody->GetNativePointer()); + PHYSX_SCENE_READ_LOCK(pxRigidStatic->getScene()); + + physx::PxShape* shape = nullptr; + pxRigidStatic->getShapes(&shape, 1, 0); + + physx::PxHeightFieldGeometry heightfieldGeometry; + shape->getHeightFieldGeometry(heightfieldGeometry); + + physx::PxHeightField* heightfield = heightfieldGeometry.heightField; + + AZStd::vector physicsSurfaceTypes; + for (Physics::MaterialId materialId : GetMaterialList()) + { + physicsSurfaceTypes.emplace_back(materialId.GetUuid().ToString()); + } + + // PhysX Heightfield cooking doesn't map 1-1 sample material indices to triangle material indices + // Hence hardcoding the expected material indices in the test + const int physicsMaterialsValidationDataIndex[] = {0, 2, 1, 1}; + + for (int sampleRow = 0; sampleRow < numRows; ++sampleRow) + { + for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn) + { + physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn); + Physics::HeightMaterialPoint samplePhysics = samples[sampleRow * numColumns + sampleColumn]; + + auto [materialIndex0, materialIndex1] = PhysX::Utils::GetPhysXMaterialIndicesFromHeightfieldSamples(samples, sampleRow, sampleColumn, numRows, numColumns); + EXPECT_EQ(samplePhysX.materialIndex0, materialIndex0); + EXPECT_EQ(samplePhysX.materialIndex1, materialIndex1); + + if (sampleRow != numRows - 1 && sampleColumn != numColumns - 1) + { + const float x_offset = -0.25f; + const float y_offset = 0.75f; + const float secondRayOffset = 0.5f; + + float rayX = x_offset + sampleColumn; + float rayY = y_offset + sampleRow; + + Physics::Material* mat1 = GetMaterialFromRaycast(rayX, rayY); + EXPECT_NE(mat1, nullptr); + + Physics::Material* mat2 = GetMaterialFromRaycast(rayX + secondRayOffset, rayY + secondRayOffset); + EXPECT_NE(mat2, nullptr); + + AZStd::string expectedMaterialName = physicsSurfaceTypes[physicsMaterialsValidationDataIndex[sampleRow * 2 + sampleColumn]]; + EXPECT_EQ(mat1->GetSurfaceTypeName(), expectedMaterialName); + } + } + } } } // namespace PhysXEditorTests diff --git a/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp b/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp index fa794c58a5..272d9157b2 100644 --- a/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp +++ b/Gems/PhysX/Code/Tests/EditorTestUtilities.cpp @@ -70,6 +70,24 @@ namespace PhysXEditorTests } } + void PhysXEditorFixture::ConnectToPVD() + { + auto* debug = AZ::Interface::Get(); + if (debug) + { + debug->ConnectToPvd(); + } + } + + void PhysXEditorFixture::DisconnectFromPVD() + { + auto* debug = AZ::Interface::Get(); + if (debug) + { + debug->DisconnectFromPvd(); + } + } + // DefaultWorldBus AzPhysics::SceneHandle PhysXEditorFixture::GetDefaultSceneHandle() const { diff --git a/Gems/PhysX/Code/Tests/EditorTestUtilities.h b/Gems/PhysX/Code/Tests/EditorTestUtilities.h index 9dfe40f366..26566f724a 100644 --- a/Gems/PhysX/Code/Tests/EditorTestUtilities.h +++ b/Gems/PhysX/Code/Tests/EditorTestUtilities.h @@ -48,6 +48,9 @@ namespace PhysXEditorTests void SetUp() override; void TearDown() override; + void ConnectToPVD(); + void DisconnectFromPVD(); + // DefaultWorldBus AzPhysics::SceneHandle GetDefaultSceneHandle() const override; From cc50a18fe997eeccc342d63f913942581d2fc9ae Mon Sep 17 00:00:00 2001 From: greerdv Date: Thu, 20 Jan 2022 11:38:22 +0000 Subject: [PATCH 16/37] add support for quad shapes in shape collider component Signed-off-by: greerdv --- .../Source/EditorShapeColliderComponent.cpp | 133 +++++++++++++++++- .../Source/EditorShapeColliderComponent.h | 11 ++ .../Code/Source/ShapeColliderComponent.h | 1 + 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp index 59b659a0bf..f61b2fce74 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +65,17 @@ namespace PhysX return AZ::Edit::PropertyVisibility::Hide; } + AZ::Crc32 EditorShapeColliderComponent::SingleSidedVisibility() + { + if ((m_shapeType == ShapeType::QuadSingleSided || m_shapeType == ShapeType::QuadDoubleSided) + && GetEntity()->FindComponent() == nullptr) + { + return AZ::Edit::PropertyVisibility::Show; + } + + return AZ::Edit::PropertyVisibility::Hide; + } + void EditorShapeColliderComponent::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) @@ -74,6 +86,7 @@ namespace PhysX ->Field("DebugDrawSettings", &EditorShapeColliderComponent::m_colliderDebugDraw) ->Field("ShapeConfigs", &EditorShapeColliderComponent::m_shapeConfigs) ->Field("SubdivisionCount", &EditorShapeColliderComponent::m_subdivisionCount) + ->Field("SingleSided", &EditorShapeColliderComponent::m_singleSided) ; if (auto editContext = serializeContext->GetEditContext()) @@ -100,6 +113,10 @@ namespace PhysX ->Attribute(AZ::Edit::Attributes::Max, Utils::MaxFrustumSubdivisions) ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorShapeColliderComponent::OnSubdivisionCountChange) ->Attribute(AZ::Edit::Attributes::Visibility, &EditorShapeColliderComponent::SubdivisionCountVisibility) + ->DataElement(AZ::Edit::UIHandlers::Default, &EditorShapeColliderComponent::m_singleSided, "Single sided", + "If enabled, planar shapes will only collide from one direction (not valid for shapes attached to dynamic rigid bodies)") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorShapeColliderComponent::OnSingleSidedChange) + ->Attribute(AZ::Edit::Attributes::Visibility, &EditorShapeColliderComponent::SingleSidedVisibility) ; } } @@ -126,7 +143,6 @@ namespace PhysX incompatible.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); incompatible.push_back(AZ_CRC_CE("CompoundShapeService")); incompatible.push_back(AZ_CRC_CE("DiskShapeService")); - incompatible.push_back(AZ_CRC_CE("QuadShapeService")); incompatible.push_back(AZ_CRC_CE("TubeShapeService")); incompatible.push_back(AZ_CRC_CE("ReferenceShapeService")); } @@ -206,6 +222,12 @@ namespace PhysX break; } + case ShapeType::QuadSingleSided: + case ShapeType::QuadDoubleSided: + { + // a quad shape has no interior, just return an empty vector of sample points + break; + } default: AZ_WarningOnce("PhysX Shape Collider Component", false, "Unsupported shape type in UpdateCachedSamplePoints"); break; @@ -331,6 +353,11 @@ namespace PhysX UpdateCylinderConfig(uniformScale); } + else if (shapeCrc == ShapeConstants::Quad) + { + UpdateQuadConfig(overallScale); + } + else { m_shapeType = !shapeCrc ? ShapeType::None : ShapeType::Unsupported; @@ -354,6 +381,60 @@ namespace PhysX m_geometryCache.m_boxDimensions = scale * boxDimensions; } + void EditorShapeColliderComponent::UpdateQuadConfig(const AZ::Vector3& scale) + { + LmbrCentral::QuadShapeConfig quadShapeConfig; + LmbrCentral::QuadShapeComponentRequestBus::EventResult(quadShapeConfig, GetEntityId(), + &LmbrCentral::QuadShapeComponentRequests::GetQuadConfiguration); + + const float minDimension = 1e-3f; // used to prevent the dimensions being 0 in any direction + const float xDim = AZ::GetMax(minDimension, quadShapeConfig.m_width); + const float yDim = AZ::GetMax(minDimension, quadShapeConfig.m_height); + + if (m_singleSided) + { + AZStd::vector cookedData; + + constexpr AZ::u32 vertexCount = 4; + const AZ::Vector3 vertices[vertexCount] = + { + AZ::Vector3(-0.5f * xDim, -0.5f * yDim, 0.0f), + AZ::Vector3(-0.5f * xDim, 0.5f * yDim, 0.0f), + AZ::Vector3(0.5f * xDim, 0.5f * yDim, 0.0f), + AZ::Vector3(0.5f * xDim, -0.5f * yDim, 0.0f), + }; + + constexpr AZ::u32 indexCount = 6; + const AZ::u32 indices[indexCount] = + { + 0, 1, 2, + 0, 2, 3 + }; + + bool cookingResult = false; + Physics::SystemRequestBus::BroadcastResult(cookingResult, &Physics::SystemRequests::CookTriangleMeshToMemory, + vertices, vertexCount, indices, indexCount, cookedData); + + Physics::CookedMeshShapeConfiguration shapeConfig; + shapeConfig.SetCookedMeshData(cookedData.data(), cookedData.size(), + Physics::CookedMeshShapeConfiguration::MeshType::TriangleMesh); + shapeConfig.m_scale = scale; + + SetShapeConfig(ShapeType::QuadSingleSided, shapeConfig); + } + + else + { + // it's not possible to create a perfectly 2d convex in PhysX, so the best we can do is a very thin box + const float zDim = AZ::GetMax(minDimension, 1e-3f * AZ::GetMin(xDim, yDim)); + const AZ::Vector3 boxDimensions(xDim, yDim, zDim); + + SetShapeConfig(ShapeType::QuadDoubleSided, Physics::BoxShapeConfiguration(boxDimensions)); + + m_shapeConfigs.back()->m_scale = scale; + } + } + void EditorShapeColliderComponent::UpdateCapsuleConfig(const AZ::Vector3& scale) { LmbrCentral::CapsuleShapeConfig lmbrCentralCapsuleShapeConfig; @@ -425,6 +506,12 @@ namespace PhysX } } + void EditorShapeColliderComponent::OnSingleSidedChange() + { + UpdateShapeConfigs(); + CreateStaticEditorCollider(); + } + AZ::u32 EditorShapeColliderComponent::OnSubdivisionCountChange() { const AZ::Vector3 uniformScale = Utils::GetUniformScale(GetEntityId()); @@ -615,6 +702,8 @@ namespace PhysX m_editorSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName); } + UpdateTriggerSettings(); + UpdateSingleSidedSettings(); UpdateShapeConfigs(); // Debug drawing @@ -767,6 +856,7 @@ namespace PhysX if (changeReason == LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged) { UpdateShapeConfigs(); + UpdateTriggerSettings(); CreateStaticEditorCollider(); Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); @@ -849,4 +939,45 @@ namespace PhysX { return m_colliderConfig.m_isTrigger; } + + void EditorShapeColliderComponent::UpdateTriggerSettings() + { + if (m_shapeType == ShapeType::QuadSingleSided || m_shapeType == ShapeType::QuadDoubleSided) + { + if (!m_previousIsTrigger.has_value()) + { + m_previousIsTrigger = m_colliderConfig.m_isTrigger; + } + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::PropertyVisibility::IsTrigger, false); + } + else + { + if (m_previousIsTrigger.has_value()) + { + m_colliderConfig.m_isTrigger = m_previousIsTrigger.value(); + m_previousIsTrigger.reset(); + } + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::PropertyVisibility::IsTrigger, true); + } + } + + void EditorShapeColliderComponent::UpdateSingleSidedSettings() + { + if (GetEntity()->FindComponent()) + { + if (!m_previousSingleSided.has_value()) + { + m_previousSingleSided = m_singleSided; + } + m_singleSided = false; + } + else + { + if (m_previousSingleSided.has_value()) + { + m_singleSided = m_previousSingleSided.value(); + m_previousSingleSided.reset(); + } + } + } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h index 3422d4959f..0f5350b781 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h @@ -41,6 +41,8 @@ namespace PhysX Sphere, PolygonPrism, Cylinder, + QuadDoubleSided, + QuadSingleSided, Unsupported }; @@ -89,6 +91,7 @@ namespace PhysX AZ::u32 OnConfigurationChanged(); void UpdateShapeConfigs(); void UpdateBoxConfig(const AZ::Vector3& scale); + void UpdateQuadConfig(const AZ::Vector3& scale); void UpdateCapsuleConfig(const AZ::Vector3& scale); void UpdateSphereConfig(const AZ::Vector3& scale); void UpdateCylinderConfig(const AZ::Vector3& scale); @@ -103,6 +106,8 @@ namespace PhysX AZ::u32 OnSubdivisionCountChange(); AZ::Crc32 SubdivisionCountVisibility(); + void OnSingleSidedChange(); + AZ::Crc32 SingleSidedVisibility(); // AZ::Component void Activate() override; @@ -137,6 +142,9 @@ namespace PhysX AZ::Aabb GetColliderShapeAabb() override; bool IsTrigger() override; + void UpdateTriggerSettings(); + void UpdateSingleSidedSettings(); + Physics::ColliderConfiguration m_colliderConfig; //!< Stores collision layers, whether the collider is a trigger, etc. DebugDraw::Collider m_colliderDebugDraw; //!< Handles drawing the collider based on global and local AzPhysics::SceneInterface* m_sceneInterface = nullptr; @@ -151,6 +159,9 @@ namespace PhysX //! @note 16 is the number of subdivisions in the debug cylinder that is loaded as a mesh (not generated procedurally) AZ::u8 m_subdivisionCount = 16; mutable GeometryCache m_geometryCache; //!< Cached data for generating sample points inside the attached shape. + AZStd::optional m_previousIsTrigger; //!< Stores the previous trigger setting if the shape is changed to one which does not support triggers. + bool m_singleSided = false; //!< Used for 2d shapes like quad which may be treated as either single or doubled sided. + AZStd::optional m_previousSingleSided; //!< Stores the previous single sided setting when unable to support single-sided shapes (such as when used with a dynamic rigid body). AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physXConfigChangedHandler; AzPhysics::SystemEvents::OnMaterialLibraryChangedEvent::Handler m_onMaterialLibraryChangedEventHandler; diff --git a/Gems/PhysX/Code/Source/ShapeColliderComponent.h b/Gems/PhysX/Code/Source/ShapeColliderComponent.h index 2065f17dcf..cfc51f08ed 100644 --- a/Gems/PhysX/Code/Source/ShapeColliderComponent.h +++ b/Gems/PhysX/Code/Source/ShapeColliderComponent.h @@ -30,6 +30,7 @@ namespace PhysX static const AZ::Crc32 Sphere = AZ_CRC("Sphere", 0x55f96687); static const AZ::Crc32 PolygonPrism = AZ_CRC("PolygonPrism", 0xd6b50036); static const AZ::Crc32 Cylinder = AZ_CRC("Cylinder", 0x9b045bea); + static const AZ::Crc32 Quad = AZ_CRC("QuadShape", 0x40d75e14); } // namespace ShapeConstants /// Component that provides a collider based on geometry from a shape component. From a8eb5be2e6f3952ce1fdff147104dd1e1e049d7a Mon Sep 17 00:00:00 2001 From: greerdv Date: Thu, 20 Jan 2022 13:24:01 +0000 Subject: [PATCH 17/37] fix order of triangle indices for single-sided quad collider Signed-off-by: greerdv --- Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp index f61b2fce74..b935ff2536 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp @@ -407,8 +407,8 @@ namespace PhysX constexpr AZ::u32 indexCount = 6; const AZ::u32 indices[indexCount] = { - 0, 1, 2, - 0, 2, 3 + 0, 2, 1, + 0, 3, 2 }; bool cookingResult = false; From f61011f5ac8382c7db145538f873933a64e76b51 Mon Sep 17 00:00:00 2001 From: greerdv Date: Thu, 20 Jan 2022 18:14:33 +0000 Subject: [PATCH 18/37] add editor side tests for quad colliders Signed-off-by: greerdv --- .../Source/EditorShapeColliderComponent.cpp | 3 +- .../Tests/ShapeColliderComponentTests.cpp | 150 +++++++++++++++++- 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp index b935ff2536..bdf70fc227 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp @@ -702,9 +702,9 @@ namespace PhysX m_editorSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName); } - UpdateTriggerSettings(); UpdateSingleSidedSettings(); UpdateShapeConfigs(); + UpdateTriggerSettings(); // Debug drawing m_colliderDebugDraw.Connect(GetEntityId()); @@ -947,6 +947,7 @@ namespace PhysX if (!m_previousIsTrigger.has_value()) { m_previousIsTrigger = m_colliderConfig.m_isTrigger; + m_colliderConfig.m_isTrigger = false; } m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::PropertyVisibility::IsTrigger, false); } diff --git a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp index 06ba4f3e9c..21d381a259 100644 --- a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -452,21 +453,55 @@ namespace PhysXEditorTests EXPECT_TRUE(aabb.GetMin().IsClose(translation - 0.5f * scale * boxDimensions)); } - void SetTrigger(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent, bool isTrigger) + void SetBoolValueOnComponent(AZ::Component* component, AZ::Crc32 name, bool value) { AZ::SerializeContext* serializeContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext); AzToolsFramework::InstanceDataHierarchy instanceDataHierarchy; - instanceDataHierarchy.AddRootInstance(editorShapeColliderComponent); + instanceDataHierarchy.AddRootInstance(component); instanceDataHierarchy.Build(serializeContext, AZ::SerializeContext::ENUM_ACCESS_FOR_WRITE); AzToolsFramework::InstanceDataHierarchy::InstanceDataNode* instanceNode = - instanceDataHierarchy.FindNodeByPartialAddress({ AZ_CRC("Trigger", 0x1a6b0f5d) }); + instanceDataHierarchy.FindNodeByPartialAddress({ name }); if (instanceNode) { - instanceNode->Write(isTrigger); + instanceNode->Write(value); } } + void SetTrigger(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent, bool isTrigger) + { + SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC("Trigger", 0x1a6b0f5d), isTrigger); + } + + bool GetBoolValueFromComponent(AZ::Component* component, AZ::Crc32 name) + { + AZ::SerializeContext* serializeContext = nullptr; + AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationRequests::GetSerializeContext); + AzToolsFramework::InstanceDataHierarchy instanceDataHierarchy; + instanceDataHierarchy.AddRootInstance(component); + instanceDataHierarchy.Build(serializeContext, AZ::SerializeContext::ENUM_ACCESS_FOR_READ); + AzToolsFramework::InstanceDataHierarchy::InstanceDataNode* instanceNode = + instanceDataHierarchy.FindNodeByPartialAddress({ name }); + bool value = false; + instanceNode->Read(value); + return value; + } + + bool IsTrigger(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent) + { + return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC("Trigger")); + } + + void SetSingleSided(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent, bool singleSided) + { + SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC("SingleSided"), singleSided); + } + + bool IsSingleSided(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent) + { + return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC("SingleSided")); + } + EntityPtr CreateRigidBox(const AZ::Vector3& boxDimensions, const AZ::Vector3& position) { EntityPtr rigidBodyEditorEntity = CreateInactiveEditorEntity("RigidBodyEditorEntity"); @@ -566,4 +601,111 @@ namespace PhysXEditorTests EXPECT_THAT(aabb.GetMin(), UnitTest::IsClose(-0.5f * boxDimensions * parentScale)); } + class PhysXEditorParamBoolFixture + : public ::testing::WithParamInterface + , public PhysXEditorFixture + { + }; + + TEST_P(PhysXEditorParamBoolFixture, EditorShapeColliderComponent_ShapeColliderWithQuadShapeNonUniformlyScalesCorrectly) + { + // test both single and double-sided quad colliders + bool singleSided = GetParam(); + + EntityPtr editorEntity = CreateInactiveEditorEntity("QuadEntity"); + editorEntity->CreateComponent(LmbrCentral::EditorQuadShapeComponentTypeId); + auto* shapeColliderComponent = editorEntity->CreateComponent(); + SetSingleSided(shapeColliderComponent, singleSided); + editorEntity->CreateComponent(); + const auto entityId = editorEntity->GetId(); + + editorEntity->Activate(); + + LmbrCentral::QuadShapeComponentRequestBus::Event(entityId, &LmbrCentral::QuadShapeComponentRequests::SetQuadWidth, 1.2f); + LmbrCentral::QuadShapeComponentRequestBus::Event(entityId, &LmbrCentral::QuadShapeComponentRequests::SetQuadHeight, 0.8f); + + // update the transform scale and non-uniform scale + AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetLocalUniformScale, 3.0f); + AZ::NonUniformScaleRequestBus::Event(entityId, &AZ::NonUniformScaleRequests::SetScale, AZ::Vector3(1.5f, 0.5f, 1.0f)); + + // make a game entity and check that its AABB is as expected + EntityPtr gameEntity = CreateActiveGameEntityFromEditorEntity(editorEntity.get()); + AZ::Aabb aabb = gameEntity->FindComponent()->GetAabb(); + + EXPECT_NEAR(aabb.GetMin().GetX(), -2.7f, 1e-3f); + EXPECT_NEAR(aabb.GetMin().GetY(), -0.6f, 1e-3f); + EXPECT_NEAR(aabb.GetMax().GetX(), 2.7f, 1e-3f); + EXPECT_NEAR(aabb.GetMax().GetY(), 0.6f, 1e-3f); + EXPECT_TRUE(true); + } + + TEST_P(PhysXEditorParamBoolFixture, EditorShapeColliderComponent_TriggerSettingIsRememberedWhenSwitchingToQuadAndBack) + { + bool initialTriggerSetting = GetParam(); + + // create an editor entity with a box component (which does support trigger) + EntityPtr editorEntity = CreateInactiveEditorEntity("QuadEntity"); + auto* boxShapeComponent = editorEntity->CreateComponent(LmbrCentral::EditorBoxShapeComponentTypeId); + auto* shapeColliderComponent = editorEntity->CreateComponent(); + SetTrigger(shapeColliderComponent, initialTriggerSetting); + editorEntity->Activate(); + + // the trigger setting should be what it was set to + EXPECT_EQ(IsTrigger(shapeColliderComponent), initialTriggerSetting); + + // deactivate the entity and swap the box for a quad (which does not support trigger) + editorEntity->Deactivate(); + editorEntity->RemoveComponent(boxShapeComponent); + auto* quadShapeComponent = editorEntity->CreateComponent(LmbrCentral::EditorQuadShapeComponentTypeId); + editorEntity->Activate(); + + // the trigger setting should now be false, because quad shape does not support triggers + EXPECT_FALSE(IsTrigger(shapeColliderComponent)); + + // swap back to a box shape + editorEntity->Deactivate(); + editorEntity->RemoveComponent(quadShapeComponent); + editorEntity->AddComponent(boxShapeComponent); + editorEntity->Activate(); + + // the original trigger setting should have been remembered + EXPECT_EQ(IsTrigger(shapeColliderComponent), initialTriggerSetting); + + // the quad shape component is no longer attached to the entity so won't be automatically cleared up + delete quadShapeComponent; + } + + TEST_P(PhysXEditorParamBoolFixture, EditorShapeColliderComponent_SingleSidedSettingIsRememberedWhenAddingAndRemovingRigidBody) + { + bool initialSingleSidedSetting = GetParam(); + + // create an editor entity without a rigid body (that means both single-sided and double-sided quads are valid) + EntityPtr editorEntity = CreateInactiveEditorEntity("QuadEntity"); + editorEntity->CreateComponent(LmbrCentral::EditorQuadShapeComponentTypeId); + auto* shapeColliderComponent = editorEntity->CreateComponent(); + SetSingleSided(shapeColliderComponent, initialSingleSidedSetting); + editorEntity->Activate(); + + // verify that the single sided setting matches the initial value + EXPECT_EQ(IsSingleSided(shapeColliderComponent), initialSingleSidedSetting); + + // add an editor rigid body component (this should mean single-sided quads are not supported) + editorEntity->Deactivate(); + auto rigidBodyComponent = editorEntity->CreateComponent(); + editorEntity->Activate(); + + EXPECT_FALSE(IsSingleSided(shapeColliderComponent)); + + // remove the editor rigid body component (the previous single-sided setting should be restored) + editorEntity->Deactivate(); + editorEntity->RemoveComponent(rigidBodyComponent); + editorEntity->Activate(); + + EXPECT_EQ(IsSingleSided(shapeColliderComponent), initialSingleSidedSetting); + + // the rigid body component is no longer attached to the entity so won't be automatically cleared up + delete rigidBodyComponent; + } + + INSTANTIATE_TEST_CASE_P(PhysXEditorTests, PhysXEditorParamBoolFixture, ::testing::Bool()); } // namespace PhysXEditorTests From 6a700e6b955d784b814cf7d39aa1ed65244236fa Mon Sep 17 00:00:00 2001 From: greerdv Date: Thu, 20 Jan 2022 19:10:56 +0000 Subject: [PATCH 19/37] add test for runtime behaviour of single sided quad collider Signed-off-by: greerdv --- .../Tests/ShapeColliderComponentTests.cpp | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp index 21d381a259..c164e5de63 100644 --- a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp @@ -707,5 +707,39 @@ namespace PhysXEditorTests delete rigidBodyComponent; } - INSTANTIATE_TEST_CASE_P(PhysXEditorTests, PhysXEditorParamBoolFixture, ::testing::Bool()); + INSTANTIATE_TEST_CASE_P(PhysXEditorTest, PhysXEditorParamBoolFixture, ::testing::Bool()); + + TEST_F(PhysXEditorFixture, EditorShapeColliderComponent_SingleSidedQuadDoesNotCollideFromBelow) + { + // create an editor entity without a rigid body (that means both single-sided and double-sided quads are valid), positioned at the origin + EntityPtr editorQuadEntity = CreateInactiveEditorEntity("QuadEntity"); + editorQuadEntity->CreateComponent(LmbrCentral::EditorQuadShapeComponentTypeId); + auto* shapeColliderComponent = editorQuadEntity->CreateComponent(); + SetSingleSided(shapeColliderComponent, true); + editorQuadEntity->Activate(); + LmbrCentral::QuadShapeComponentRequestBus::Event(editorQuadEntity->GetId(), &LmbrCentral::QuadShapeComponentRequests::SetQuadHeight, 10.0f); + LmbrCentral::QuadShapeComponentRequestBus::Event(editorQuadEntity->GetId(), &LmbrCentral::QuadShapeComponentRequests::SetQuadWidth, 10.0f); + + // add a second entity with a box collider and a rigid body, positioned below the quad + EntityPtr editorBoxEntity = CreateInactiveEditorEntity("BoxEntity"); + editorBoxEntity->CreateComponent(LmbrCentral::BoxShapeComponentTypeId); + editorBoxEntity->CreateComponent(); + editorBoxEntity->CreateComponent(); + editorBoxEntity->Activate(); + AZ::TransformBus::Event(editorBoxEntity->GetId(), &AZ::TransformBus::Events::SetWorldTranslation, -AZ::Vector3::CreateAxisZ()); + + EntityPtr gameQuadEntity = CreateActiveGameEntityFromEditorEntity(editorQuadEntity.get()); + EntityPtr gameBoxEntity = CreateActiveGameEntityFromEditorEntity(editorBoxEntity.get()); + + // give the box enough upward velocity to rise above the level of the quad and simulate + Physics::RigidBodyRequestBus::Event(gameBoxEntity->GetId(), &Physics::RigidBodyRequests::SetLinearVelocity, AZ::Vector3::CreateAxisZ(6.0f)); + PhysX::TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 200); + + // the box should travel through the base of the quad because it has no collision from that direction + // and land on the top surface of the quad, which does have collision + float finalHeight = 0.0f; + AZ::TransformBus::EventResult(finalHeight, gameBoxEntity->GetId(), &AZ::TransformBus::Events::GetWorldZ); + + EXPECT_GT(finalHeight, 0.0f); + } } // namespace PhysXEditorTests From e2d157006c260e99bc3e122d7368de04f5a280af Mon Sep 17 00:00:00 2001 From: evanchia Date: Thu, 20 Jan 2022 15:57:09 -0800 Subject: [PATCH 20/37] Temporary fix for editor log stomping during python tests Signed-off-by: evanchia --- Code/Editor/CryEdit.cpp | 2 -- Tools/LyTestTools/ly_test_tools/o3de/editor_test.py | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 1c4e22b99e..cf233dc285 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -2134,8 +2134,6 @@ bool CCryEditApp::FixDanglingSharedMemory(const QString& sharedMemName) const int CCryEditApp::ExitInstance(int exitCode) { - AZ_TracePrintf("Exit", "Called ExitInstance() with exit code: 0x%x", exitCode); - if (m_pEditor) { m_pEditor->OnBeginShutdownSequence(); diff --git a/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py b/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py index 8bd4fbe29a..672259b076 100644 --- a/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py +++ b/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py @@ -315,7 +315,7 @@ class EditorTestSuite(): return 8 ## Internal ## - _TIMEOUT_CRASH_LOG = 20 # Maximum time (seconds) for waiting for a crash file, in secondss + _TIMEOUT_CRASH_LOG = 20 # Maximum time (seconds) for waiting for a crash file, in seconds _TEST_FAIL_RETCODE = 0xF # Return code for test failure @pytest.fixture(scope="class") @@ -772,7 +772,8 @@ class EditorTestSuite(): return_code = editor.get_returncode() editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace) # Save the editor log - workspace.artifact_manager.save_artifact(os.path.join(editor_utils.retrieve_log_path(run_id, workspace), log_name)) + workspace.artifact_manager.save_artifact(os.path.join(editor_utils.retrieve_log_path(run_id, workspace), log_name), + f'({run_id}){log_name}') if return_code == 0: test_result = Result.Pass.create(test_spec, output, editor_log_content) else: @@ -847,7 +848,8 @@ class EditorTestSuite(): return_code = editor.get_returncode() editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace) # Save the editor log - workspace.artifact_manager.save_artifact(os.path.join(editor_utils.retrieve_log_path(run_id, workspace), log_name)) + workspace.artifact_manager.save_artifact(os.path.join(editor_utils.retrieve_log_path(run_id, workspace), log_name), + f'({run_id}){log_name}') if return_code == 0: # No need to scrap the output, as all the tests have passed for test_spec in test_spec_list: From 56c6f81627fcc957f30a46f5e818cad7558c79c8 Mon Sep 17 00:00:00 2001 From: Sergey Pereslavtsev Date: Fri, 21 Jan 2022 13:21:57 +0000 Subject: [PATCH 21/37] PR addressing. Removed RTTI from HeightMaterialPoint since it's just a struct of data with no intention being a part of class hierarchy Signed-off-by: Sergey Pereslavtsev --- .../Physics/HeightfieldProviderBus.h | 5 +- Gems/PhysX/Code/Source/Utils.cpp | 85 ++++++++++--------- ...ditorHeightfieldColliderComponentTests.cpp | 13 +-- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h index da361f0a2b..585fb59ad6 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h @@ -35,16 +35,15 @@ namespace Physics { } - virtual ~HeightMaterialPoint() = default; + ~HeightMaterialPoint() = default; static void Reflect(AZ::ReflectContext* context); - AZ_RTTI(HeightMaterialPoint, "{DF167ED4-24E6-4F7B-8AB7-42622F7DBAD3}"); + AZ_TYPE_INFO(HeightMaterialPoint, "{DF167ED4-24E6-4F7B-8AB7-42622F7DBAD3}"); float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location. QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner. uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad. uint16_t m_padding{ 0 }; //!< available for future use. - }; //! An interface to provide heightfield values. diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index a988f6791c..950404820d 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -71,7 +71,6 @@ namespace PhysX const int32_t row, const int32_t col, const int32_t numRows, const int32_t numCols) { - uint8_t materialIndex0 = 0; uint8_t materialIndex1 = 0; @@ -81,49 +80,51 @@ namespace PhysX // In PhysX, the material indices refer to the quad down and to the right of the sample. // If we're in the last row or last column, there aren't any quads down or to the right, // so just clear these out. - - if (!lastRowIndex && !lastColumnIndex) + if (lastRowIndex || lastColumnIndex) { - auto GetIndex = [numCols](int32_t row, int32_t col) - { - return (row * numCols) + col; - }; - - // Our source data is providing one material index per vertex, but PhysX wants one material index - // per triangle. The heuristic that we'll go with for selecting the material index is to choose - // the material for the vertex that's not on the diagonal of each triangle. - // Ex: A *---* B - // | / | For this, we'll use A for index0 and D for index1. - // C *---* D - // - // Ex: A *---* B - // | \ | For this, we'll use C for index0 and B for index1. - // C *---* D - // - // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this - // causes incorrect or unpredictable physics material mappings. - - const Physics::HeightMaterialPoint& currentSample = samples[GetIndex(row, col)]; - - switch (currentSample.m_quadMeshType) - { - case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: - materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; - materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; - break; - case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: - materialIndex0 = currentSample.m_materialIndex; - materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; - break; - case Physics::QuadMeshType::Hole: - materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; - materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; - break; - default: - AZ_Assert(false, "Unhandled case in GetPhysXMaterialIndicesFromHeightfieldSamples"); - break; - } + return { materialIndex0, materialIndex1 }; } + + auto GetIndex = [numCols](int32_t row, int32_t col) + { + return (row * numCols) + col; + }; + + // Our source data is providing one material index per vertex, but PhysX wants one material index + // per triangle. The heuristic that we'll go with for selecting the material index is to choose + // the material for the vertex that's not on the diagonal of each triangle. + // Ex: A *---* B + // | / | For this, we'll use A for index0 and D for index1. + // C *---* D + // + // Ex: A *---* B + // | \ | For this, we'll use C for index0 and B for index1. + // C *---* D + // + // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this + // causes incorrect or unpredictable physics material mappings. + + const Physics::HeightMaterialPoint& currentSample = samples[GetIndex(row, col)]; + + switch (currentSample.m_quadMeshType) + { + case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: + materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; + materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: + materialIndex0 = currentSample.m_materialIndex; + materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::Hole: + materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; + materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; + break; + default: + AZ_Assert(false, "Unhandled case in GetPhysXMaterialIndicesFromHeightfieldSamples"); + break; + } + return { materialIndex0, materialIndex1 }; } diff --git a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp index 9bdc63e60d..9a216b7d80 100644 --- a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp @@ -139,9 +139,9 @@ namespace PhysXEditorTests // Create an asset out of our Script Event Physics::MaterialLibraryAsset* matLibAsset = aznew Physics::MaterialLibraryAsset; { - AZStd::vector matIds = GetMaterialList(); + const AZStd::vector matIds = GetMaterialList(); - for (Physics::MaterialId matId : matIds) + for (const Physics::MaterialId& matId : matIds) { Physics::MaterialFromAssetConfiguration matConfig; matConfig.m_id = matId; @@ -336,7 +336,7 @@ namespace PhysXEditorTests // PhysX Heightfield cooking doesn't map 1-1 sample material indices to triangle material indices // Hence hardcoding the expected material indices in the test - const int physicsMaterialsValidationDataIndex[] = {0, 2, 1, 1}; + const AZStd::array physicsMaterialsValidationDataIndex = {0, 2, 1, 1}; for (int sampleRow = 0; sampleRow < numRows; ++sampleRow) { @@ -364,8 +364,11 @@ namespace PhysXEditorTests Physics::Material* mat2 = GetMaterialFromRaycast(rayX + secondRayOffset, rayY + secondRayOffset); EXPECT_NE(mat2, nullptr); - AZStd::string expectedMaterialName = physicsSurfaceTypes[physicsMaterialsValidationDataIndex[sampleRow * 2 + sampleColumn]]; - EXPECT_EQ(mat1->GetSurfaceTypeName(), expectedMaterialName); + if (mat1) + { + AZStd::string expectedMaterialName = physicsSurfaceTypes[physicsMaterialsValidationDataIndex[sampleRow * 2 + sampleColumn]]; + EXPECT_EQ(mat1->GetSurfaceTypeName(), expectedMaterialName); + } } } } From b21878d04e78e02f430c660b4cc9c9c5bda361a8 Mon Sep 17 00:00:00 2001 From: Sergey Pereslavtsev Date: Fri, 21 Jan 2022 19:57:28 +0000 Subject: [PATCH 22/37] Removed unused variable Signed-off-by: Sergey Pereslavtsev --- .../PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp index 9a216b7d80..5f830acc8a 100644 --- a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp @@ -343,7 +343,6 @@ namespace PhysXEditorTests for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn) { physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn); - Physics::HeightMaterialPoint samplePhysics = samples[sampleRow * numColumns + sampleColumn]; auto [materialIndex0, materialIndex1] = PhysX::Utils::GetPhysXMaterialIndicesFromHeightfieldSamples(samples, sampleRow, sampleColumn, numRows, numColumns); EXPECT_EQ(samplePhysX.materialIndex0, materialIndex0); From 7fc4e4bd105ab53ec2ea990fa055b581da97d666 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Fri, 21 Jan 2022 14:29:37 -0600 Subject: [PATCH 23/37] Refactored the styling of TableView header sections Signed-off-by: Chris Galvan --- .../AzQtComponents/Components/Style.cpp | 12 ------------ .../Components/Widgets/TableView.cpp | 18 ------------------ .../Components/Widgets/TableView.h | 2 -- .../Components/Widgets/TableView.qss | 2 +- .../AzQtComponents/Gallery/TableViewPage.cpp | 3 +++ 5 files changed, 4 insertions(+), 33 deletions(-) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp index ef29c05e61..849785ff01 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp @@ -411,18 +411,6 @@ namespace AzQtComponents } break; - case CE_HeaderSection: - { - if (qobject_cast(widget)) - { - if (TableView::drawHeaderSection(this, option, painter, widget, m_data->tableViewConfig)) - { - return; - } - } - } - break; - case CE_ComboBoxLabel: { if (qobject_cast(widget)) diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.cpp index f5cb90ae1a..b4b49bdcec 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.cpp @@ -38,7 +38,6 @@ namespace AzQtComponents ConfigHelpers::read(settings, QStringLiteral("FocusBorderWidth"), config.focusBorderWidth); ConfigHelpers::read(settings, QStringLiteral("FocusBorderColor"), config.focusBorderColor); ConfigHelpers::read(settings, QStringLiteral("FocusFillColor"), config.focusFillColor); - ConfigHelpers::read(settings, QStringLiteral("HeaderFillColor"), config.headerFillColor); settings.endGroup(); return config; @@ -52,7 +51,6 @@ namespace AzQtComponents config.focusBorderWidth = 1; config.focusBorderColor = QStringLiteral("#94D2FF"); config.focusFillColor = QStringLiteral("#10ffffff"); - config.headerFillColor = QStringLiteral("#2d2d2d"); return config; } @@ -177,22 +175,6 @@ namespace AzQtComponents return true; } - bool TableView::drawHeaderSection(const Style* style, const QStyleOption* option, QPainter* painter, const QWidget* widget, const Config& config) - { - Q_UNUSED(widget); - Q_UNUSED(style); - - const auto headerViewOption = qstyleoption_cast(option); - if (!headerViewOption) - { - return false; - } - - painter->fillRect(headerViewOption->rect, config.headerFillColor); - - return true; - } - bool TableView::drawFrameFocusRect(const Style* style, const QStyleOption* option, QPainter* painter, const Config& config) { Q_UNUSED(style); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.h index 99fb77c3d8..6187f44dd5 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.h @@ -62,7 +62,6 @@ namespace AzQtComponents qreal focusBorderWidth; QColor focusBorderColor; QColor focusFillColor; - QColor headerFillColor; }; /*! @@ -93,7 +92,6 @@ namespace AzQtComponents private: static bool drawHeader(const Style* style, const QStyleOption* option, QPainter* painter, const QWidget* widget, const Config& config); - static bool drawHeaderSection(const Style* style, const QStyleOption* option, QPainter* painter, const QWidget* widget, const Config& config); static bool drawFrameFocusRect(const Style* style, const QStyleOption* option, QPainter* painter, const Config& config); static QRect itemViewItemRect(const Style* style, QStyle::SubElement element, const QStyleOptionViewItem* option, const QWidget* widget, const Config& config); static QSize sizeFromContents(const Style* style, QStyle::ContentsType type, const QStyleOption* option, const QSize& contentsSize, const QWidget* widget, const Config& config); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.qss b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.qss index 47281eda7e..531a3378fa 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.qss +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Widgets/TableView.qss @@ -66,7 +66,7 @@ QTreeView QHeaderView::section { height: 24px; - background: #444444; + background: #2d2d2d; border: none; font-size: 12px; diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Gallery/TableViewPage.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Gallery/TableViewPage.cpp index 788ba62366..24e65ac540 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Gallery/TableViewPage.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Gallery/TableViewPage.cpp @@ -134,6 +134,9 @@ TableViewPage::TableViewPage(QWidget* parent) auto logItemDelegate = new AzToolsFramework::Logging::LogTableItemDelegate(ui->logTableView); ui->logTableView->setItemDelegate(logItemDelegate); + // Example of changing the header section background color + ui->logTableView->setStyleSheet("QHeaderView::section { background: transparent; }"); + ui->qTableView->setModel(logModel); ui->qTableView->setAlternatingRowColors(true); ui->qTableView->setShowGrid(false); From 7fa6a82f550a4ca687b1a7742948859ad472af4f Mon Sep 17 00:00:00 2001 From: michabr <82236305+michabr@users.noreply.github.com> Date: Fri, 21 Jan 2022 15:03:57 -0800 Subject: [PATCH 24/37] Make ILyShine an AZ::Interface (#6923) * Move ILyShine instance out of legacy code Signed-off-by: abrmich * Remove last remaining reference of gEnv->pLyShine Signed-off-by: abrmich * Make m_lyShine a unique pointer Signed-off-by: abrmich --- Code/Legacy/CryCommon/ISystem.h | 1 - .../Editor/CommandCanvasPropertiesChange.cpp | 2 +- Gems/LyShine/Code/Editor/EditorWindow.cpp | 10 ++++---- .../Editor/LyShineEditorSystemComponent.cpp | 4 +-- .../Code/Editor/SpriteBorderEditor.cpp | 3 ++- Gems/LyShine/Code/Editor/ViewportWidget.cpp | 10 ++++---- Gems/LyShine/Code/Include/LyShine/IDraw2d.h | 8 +++--- Gems/LyShine/Code/Include/LyShine/ILyShine.h | 5 +++- Gems/LyShine/Code/Source/LyShine.cpp | 14 +++++------ Gems/LyShine/Code/Source/LyShine.h | 2 +- Gems/LyShine/Code/Source/LyShineDebug.cpp | 8 +++--- .../LyShine/Code/Source/LyShineLoadScreen.cpp | 25 ++++++++++--------- .../Code/Source/LyShineSystemComponent.cpp | 20 ++++++++------- .../Code/Source/LyShineSystemComponent.h | 2 +- .../Code/Source/Script/UiCanvasLuaBus.cpp | 4 +-- .../LyShine/Code/Source/UiCanvasComponent.cpp | 8 +++--- Gems/LyShine/Code/Source/UiImageComponent.cpp | 12 ++++----- .../Code/Source/UiImageSequenceComponent.cpp | 4 +-- .../Code/Source/UiInteractableState.cpp | 8 +++--- .../Source/UiParticleEmitterComponent.cpp | 6 ++--- .../World/UiCanvasAssetRefComponent.cpp | 9 ++++--- .../Code/Tests/UiTooltipComponentTest.cpp | 5 ---- .../Code/Source/LyShineExamplesCppExample.cpp | 4 +-- .../Code/Source/UiCustomImageComponent.cpp | 6 ++--- .../Code/Source/LyShineMessagePopup.cpp | 12 ++++----- 25 files changed, 97 insertions(+), 95 deletions(-) diff --git a/Code/Legacy/CryCommon/ISystem.h b/Code/Legacy/CryCommon/ISystem.h index 6fdc8b52fe..14b78fd155 100644 --- a/Code/Legacy/CryCommon/ISystem.h +++ b/Code/Legacy/CryCommon/ISystem.h @@ -614,7 +614,6 @@ struct SSystemGlobalEnvironment ISystem* pSystem = nullptr; ILog* pLog; IMovieSystem* pMovieSystem; - ILyShine* pLyShine; SharedEnvironmentInstance* pSharedEnvironment; #if defined(AZ_RESTRICTED_PLATFORM) diff --git a/Gems/LyShine/Code/Editor/CommandCanvasPropertiesChange.cpp b/Gems/LyShine/Code/Editor/CommandCanvasPropertiesChange.cpp index 30a716b142..e90252e54a 100644 --- a/Gems/LyShine/Code/Editor/CommandCanvasPropertiesChange.cpp +++ b/Gems/LyShine/Code/Editor/CommandCanvasPropertiesChange.cpp @@ -64,7 +64,7 @@ void CommandCanvasPropertiesChange::Recreate(bool isUndo) // Create a new canvas from the XML and release the old canvas, use the new entity context for // the new canvas const AZStd::string& xml = isUndo ? m_undoXml : m_redoXml; - gEnv->pLyShine->ReloadCanvasFromXml(xml, newEntityContext); + AZ::Interface::Get()->ReloadCanvasFromXml(xml, newEntityContext); // Tell the editor window to use the new entity context m_editorWindow->ReplaceEntityContext(newEntityContext); diff --git a/Gems/LyShine/Code/Editor/EditorWindow.cpp b/Gems/LyShine/Code/Editor/EditorWindow.cpp index 7b60474ed5..8f1d574ae5 100644 --- a/Gems/LyShine/Code/Editor/EditorWindow.cpp +++ b/Gems/LyShine/Code/Editor/EditorWindow.cpp @@ -315,7 +315,7 @@ EditorWindow::~EditorWindow() // unload the preview mode canvas if it exists (e.g. if we close the editor window while in preview mode) if (m_previewModeCanvasEntityId.IsValid()) { - gEnv->pLyShine->ReleaseCanvas(m_previewModeCanvasEntityId, false); + AZ::Interface::Get()->ReleaseCanvas(m_previewModeCanvasEntityId, false); } delete m_sliceLibraryTree; @@ -564,7 +564,7 @@ bool EditorWindow::OnPreWarning(const char* /*window*/, const char* /*fileName*/ void EditorWindow::DestroyCanvas(const UiCanvasMetadata& canvasMetadata) { - gEnv->pLyShine->ReleaseCanvas(canvasMetadata.m_canvasEntityId, true); + AZ::Interface::Get()->ReleaseCanvas(canvasMetadata.m_canvasEntityId, true); } bool EditorWindow::IsCanvasTabMetadataValidForTabIndex(int index) @@ -950,14 +950,14 @@ bool EditorWindow::LoadCanvas(const QString& canvasFilename, bool autoLoad, bool bool errorsOnLoad = false; if (canvasFilename.isEmpty()) { - canvasEntityId = gEnv->pLyShine->CreateCanvasInEditor(entityContext); + canvasEntityId = AZ::Interface::Get()->CreateCanvasInEditor(entityContext); } else { // Collect errors and warnings during the canvas load AZ::Debug::TraceMessageBus::Handler::BusConnect(); - canvasEntityId = gEnv->pLyShine->LoadCanvasInEditor(assetIdPathname.c_str(), sourceAssetPathName.c_str(), entityContext); + canvasEntityId = AZ::Interface::Get()->LoadCanvasInEditor(assetIdPathname.c_str(), sourceAssetPathName.c_str(), entityContext); // Stop receiving error and warning events AZ::Debug::TraceMessageBus::Handler::BusDisconnect(); @@ -1603,7 +1603,7 @@ void EditorWindow::ToggleEditorMode() EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, m_previewModeCanvasEntityId); if (entity) { - gEnv->pLyShine->ReleaseCanvas(m_previewModeCanvasEntityId, false); + AZ::Interface::Get()->ReleaseCanvas(m_previewModeCanvasEntityId, false); } m_previewModeCanvasEntityId.SetInvalid(); } diff --git a/Gems/LyShine/Code/Editor/LyShineEditorSystemComponent.cpp b/Gems/LyShine/Code/Editor/LyShineEditorSystemComponent.cpp index 229101d4c0..3ec6980cfb 100644 --- a/Gems/LyShine/Code/Editor/LyShineEditorSystemComponent.cpp +++ b/Gems/LyShine/Code/Editor/LyShineEditorSystemComponent.cpp @@ -211,9 +211,9 @@ namespace LyShineEditor void LyShineEditorSystemComponent::OnStopPlayInEditor() { // reset UI system - if (gEnv->pLyShine) + if (AZ::Interface::Get()) { - gEnv->pLyShine->Reset(); + AZ::Interface::Get()->Reset(); } } } diff --git a/Gems/LyShine/Code/Editor/SpriteBorderEditor.cpp b/Gems/LyShine/Code/Editor/SpriteBorderEditor.cpp index 93d77bd174..b9ca4cd32c 100644 --- a/Gems/LyShine/Code/Editor/SpriteBorderEditor.cpp +++ b/Gems/LyShine/Code/Editor/SpriteBorderEditor.cpp @@ -7,6 +7,7 @@ */ #include "SpriteBorderEditorCommon.h" +#include #include #include #include @@ -35,7 +36,7 @@ SpriteBorderEditor::SpriteBorderEditor(const char* path, QWidget* parent) setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::MSWindowsFixedSizeDialogHint); // Make sure the sprite can load and be displayed before continuing - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePath.toLatin1().constData()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePath.toLatin1().constData()); QString fullPath = Path::GamePathToFullPath(m_sprite->GetTexturePathname().c_str()); const bool imageWontLoad = !m_sprite || QPixmap(fullPath).isNull(); if (imageWontLoad) diff --git a/Gems/LyShine/Code/Editor/ViewportWidget.cpp b/Gems/LyShine/Code/Editor/ViewportWidget.cpp index eb5625ac31..32231058bf 100644 --- a/Gems/LyShine/Code/Editor/ViewportWidget.cpp +++ b/Gems/LyShine/Code/Editor/ViewportWidget.cpp @@ -181,7 +181,7 @@ namespace EBUS_EVENT_ID_RESULT(handled, canvasEntityId, UiCanvasBus, HandleInputEvent, inputSnapshot, viewportPos, activeModifierKeys); // Execute events that have been queued during the input event handler - gEnv->pLyShine->ExecuteQueuedEvents(); + AZ::Interface::Get()->ExecuteQueuedEvents(); return handled; } @@ -192,7 +192,7 @@ namespace EBUS_EVENT_ID_RESULT(handled, canvasEntityId, UiCanvasBus, HandleTextEvent, textUTF8); // Execute events that have been queued during the input event handler - gEnv->pLyShine->ExecuteQueuedEvents(); + AZ::Interface::Get()->ExecuteQueuedEvents(); return handled; } @@ -265,7 +265,7 @@ ViewportWidget::~ViewportWidget() // Notify LyShine that this is no longer a valid UiRenderer. // Only one viewport/renderer is currently supported in the UI Editor - CLyShine* lyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); lyShine->SetUiRendererForEditor(nullptr); } @@ -276,7 +276,7 @@ void ViewportWidget::InitUiRenderer() // Notify LyShine that this is the UiRenderer to be used for rendering // UI canvases that are loaded in the UI Editor. // Only one viewport/renderer is currently supported in the UI Editor - CLyShine* lyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); lyShine->SetUiRendererForEditor(m_uiRenderer); m_draw2d = AZStd::make_shared(GetViewportContext()); @@ -1123,7 +1123,7 @@ void ViewportWidget::UpdatePreviewMode(float deltaTime) EBUS_EVENT_ID(canvasEntityId, UiEditorCanvasBus, UpdateCanvasInEditorViewport, deltaTime, true); // Execute events that have been queued during the canvas update - gEnv->pLyShine->ExecuteQueuedEvents(); + AZ::Interface::Get()->ExecuteQueuedEvents(); } } diff --git a/Gems/LyShine/Code/Include/LyShine/IDraw2d.h b/Gems/LyShine/Code/Include/LyShine/IDraw2d.h index 77dc5ac601..e880ca54a7 100644 --- a/Gems/LyShine/Code/Include/LyShine/IDraw2d.h +++ b/Gems/LyShine/Code/Include/LyShine/IDraw2d.h @@ -473,9 +473,9 @@ public: // static member functions //! Helper to get the default IDraw2d interface static IDraw2d* GetDefaultDraw2d() { - if (gEnv && gEnv->pLyShine) // [LYSHINE_ATOM_TODO][GHI #3569] Remove LyShine global interface pointer from legacy global environment + if (AZ::Interface::Get()) { - IDraw2d* draw2d = gEnv->pLyShine->GetDraw2d(); + IDraw2d* draw2d = AZ::Interface::Get()->GetDraw2d(); return reinterpret_cast(draw2d); } @@ -485,9 +485,9 @@ public: // static member functions //! Helper to load a texture static AZ::Data::Instance LoadTexture(const AZStd::string& pathName) { - if (gEnv && gEnv->pLyShine) // [LYSHINE_ATOM_TODO][GHI #3569] Remove LyShine global interface pointer from legacy global environment + if (AZ::Interface::Get()) { - return gEnv->pLyShine->LoadTexture(pathName); + return AZ::Interface::Get()->LoadTexture(pathName); } return nullptr; diff --git a/Gems/LyShine/Code/Include/LyShine/ILyShine.h b/Gems/LyShine/Code/Include/LyShine/ILyShine.h index 71ca6ec074..d05e105c87 100644 --- a/Gems/LyShine/Code/Include/LyShine/ILyShine.h +++ b/Gems/LyShine/Code/Include/LyShine/ILyShine.h @@ -38,7 +38,10 @@ namespace AZ::RPI class ILyShine { public: - virtual ~ILyShine(){} + AZ_RTTI(ILyShine, "{652ED9D7-0782-44E8-BCE7-65DD38C90907}"); + + ILyShine() = default; + virtual ~ILyShine() = default; //! Delete this object virtual void Release() = 0; diff --git a/Gems/LyShine/Code/Source/LyShine.cpp b/Gems/LyShine/Code/Source/LyShine.cpp index b362ed2106..df26a905ad 100644 --- a/Gems/LyShine/Code/Source/LyShine.cpp +++ b/Gems/LyShine/Code/Source/LyShine.cpp @@ -124,7 +124,7 @@ AllocateConstIntCVar(CLyShine, CV_ui_RunUnitTestsOnStartup); #endif //////////////////////////////////////////////////////////////////////////////////////////////////// -CLyShine::CLyShine([[maybe_unused]] ISystem* system) +CLyShine::CLyShine() : AzFramework::InputChannelEventListener(AzFramework::InputChannelEventListener::GetPriorityUI()) , AzFramework::InputTextEventListener(AzFramework::InputTextEventListener::GetPriorityUI()) , m_draw2d(new CDraw2d) @@ -386,8 +386,8 @@ void CLyShine::Update(float deltaTimeInSeconds) } // Tell the UI system the size of the viewport we are rendering to - this drives the - // canvas size for full screen UI canvases. It needs to be set before either pLyShine->Update or - // pLyShine->Render are called. It must match the viewport size that the input system is using. + // canvas size for full screen UI canvases. It needs to be set before either lyShine->Update or + // lyShine->Render are called. It must match the viewport size that the input system is using. SetViewportSize(m_uiRenderer->GetViewportSize()); // Guard against nested updates. This can occur if a canvas update below triggers the load screen component's @@ -708,10 +708,10 @@ void CLyShine::RenderUiCursor() //////////////////////////////////////////////////////////////////////////////////////////////////// void CLyShine::DebugReportDrawCalls(IConsoleCmdArgs* cmdArgs) { - if (gEnv->pLyShine) + if (AZ::Interface::Get()) { // We want to use an internal-only non-static function so downcast to CLyShine - CLyShine* pLyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); // There is an optional parameter which is a name to include in the output filename AZStd::string name; @@ -721,7 +721,7 @@ void CLyShine::DebugReportDrawCalls(IConsoleCmdArgs* cmdArgs) } // Use the canvas manager to access all the loaded canvases - pLyShine->m_uiCanvasManager->DebugReportDrawCalls(name); + lyShine->m_uiCanvasManager->DebugReportDrawCalls(name); } } #endif @@ -742,7 +742,7 @@ void CLyShine::RunUnitTests(IConsoleCmdArgs* cmdArgs) return; } - CLyShine* lyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); AZ_Assert(lyShine, "Attempting to run unit-tests prior to LyShine initialization!"); TextMarkup::UnitTest(cmdArgs); diff --git a/Gems/LyShine/Code/Source/LyShine.h b/Gems/LyShine/Code/Source/LyShine.h index eab52258c1..2fd0f80210 100644 --- a/Gems/LyShine/Code/Source/LyShine.h +++ b/Gems/LyShine/Code/Source/LyShine.h @@ -48,7 +48,7 @@ class CLyShine public: //! Create the LyShine object, the given system pointer is stored internally - CLyShine(ISystem* system); + CLyShine(); // ILyShine diff --git a/Gems/LyShine/Code/Source/LyShineDebug.cpp b/Gems/LyShine/Code/Source/LyShineDebug.cpp index 7e99652f45..3502699525 100644 --- a/Gems/LyShine/Code/Source/LyShineDebug.cpp +++ b/Gems/LyShine/Code/Source/LyShineDebug.cpp @@ -997,7 +997,7 @@ static AZ::Entity* CreateButton(const char* name, bool atRoot, AZ::EntityId pare EBUS_EVENT_ID(buttonId, UiInteractableStatesBus, SetStateAlpha, UiInteractableStatesInterface::StatePressed, buttonId, pressedColor.GetA()); AZStd::string pathname = "Textures/Basic/Button_Sliced_Normal.sprite"; - ISprite* sprite = gEnv->pLyShine->LoadSprite(pathname); + ISprite* sprite = AZ::Interface::Get()->LoadSprite(pathname); EBUS_EVENT_ID(buttonId, UiImageBus, SetSprite, sprite); EBUS_EVENT_ID(buttonId, UiImageBus, SetImageType, UiImageInterface::ImageType::Sliced); @@ -1096,7 +1096,7 @@ static AZ::Entity* CreateTextInput(const char* name, bool atRoot, AZ::EntityId p EBUS_EVENT_ID(textInputId, UiInteractableStatesBus, SetStateAlpha, UiInteractableStatesInterface::StatePressed, textInputId, pressedColor.GetA()); AZStd::string pathname = "Textures/Basic/Button_Sliced_Normal.sprite"; - ISprite* sprite = gEnv->pLyShine->LoadSprite(pathname); + ISprite* sprite = AZ::Interface::Get()->LoadSprite(pathname); EBUS_EVENT_ID(textInputId, UiImageBus, SetSprite, sprite); EBUS_EVENT_ID(textInputId, UiImageBus, SetImageType, UiImageInterface::ImageType::Sliced); @@ -1192,7 +1192,7 @@ static void DestroyTestCanvas() delete g_testActionListener2; g_testActionListener2 = nullptr; - gEnv->pLyShine->ReleaseCanvas(g_testCanvasId, false); + AZ::Interface::Get()->ReleaseCanvas(g_testCanvasId, false); g_testCanvasId.SetInvalid(); } } @@ -1216,7 +1216,7 @@ static void TestCanvasCreate ([[maybe_unused]] IConsoleCmdArgs* Cmd) DestroyTestCanvas(); // test creation of canvas and some simple elements - AZ::EntityId canvasEntityId = gEnv->pLyShine->CreateCanvas(); + AZ::EntityId canvasEntityId = AZ::Interface::Get()->CreateCanvas(); UiCanvasInterface* canvas = UiCanvasBus::FindFirstHandler(canvasEntityId); if (!canvas) { diff --git a/Gems/LyShine/Code/Source/LyShineLoadScreen.cpp b/Gems/LyShine/Code/Source/LyShineLoadScreen.cpp index c59cb5cb34..e0916d43b2 100644 --- a/Gems/LyShine/Code/Source/LyShineLoadScreen.cpp +++ b/Gems/LyShine/Code/Source/LyShineLoadScreen.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -63,7 +64,7 @@ namespace LyShine AZ_ErrorOnce(nullptr, false, "NotifyGameLoadStart needs to be removed/ported to use Atom"); return false; #if 0 - if (!gEnv || gEnv->pRenderer || !gEnv->pLyShine) + if (!gEnv || gEnv->pRenderer || !AZ::Interface::Get()) { return false; } @@ -103,7 +104,7 @@ namespace LyShine return false; //TODO: gEnv->pRenderer is always null, fix the logic below #if 0 - if (!gEnv || gEnv->pRenderer || !gEnv->pLyShine) + if (!gEnv || gEnv->pRenderer || !AZ::Interface::Get()) { return false; } @@ -140,22 +141,22 @@ namespace LyShine void LyShineLoadScreenComponent::UpdateAndRender([[maybe_unused]] float deltaTimeInSeconds) { AZ_Assert(m_isPlaying, "LyShineLoadScreenComponent should not be connected to LoadScreenUpdateNotificationBus while not playing"); - AZ_ErrorOnce(nullptr, m_isPlaying && gEnv && gEnv->pLyShine, "UpdateAndRender needs to be removed/ported to use Atom"); + AZ_ErrorOnce(nullptr, m_isPlaying && AZ::Interface::Get(), "UpdateAndRender needs to be removed/ported to use Atom"); //TODO: gEnv->pRenderer is always null, fix the logic below #if 0 - if (m_isPlaying && gEnv && gEnv->pLyShine && gEnv->pRenderer) + if (m_isPlaying && gEnv && AZ::Interface::Get() && gEnv->pRenderer) { AZ_Assert(GetCurrentThreadId() == gEnv->mMainThreadId, "UpdateAndRender should only be called from the main thread"); // update the animation system - gEnv->pLyShine->Update(deltaTimeInSeconds); + AZ::Interface::Get()->Update(deltaTimeInSeconds); // Render. gEnv->pRenderer->SetViewport(0, 0, gEnv->pRenderer->GetOverlayWidth(), gEnv->pRenderer->GetOverlayHeight()); gEnv->pRenderer->BeginFrame(); - gEnv->pLyShine->Render(); + AZ::Interface::Get()->Render(); gEnv->pRenderer->EndFrame(); } #endif @@ -177,7 +178,7 @@ namespace LyShine m_isPlaying = false; - if (gEnv && gEnv->pLyShine) + if (AZ::Interface::Get()) { AZ::Entity* canvasEntity = nullptr; @@ -187,8 +188,8 @@ namespace LyShine EBUS_EVENT_RESULT(canvasEntity, AZ::ComponentApplicationBus, FindEntity, m_gameCanvasEntityId); if (canvasEntity) { - gEnv->pLyShine->ReleaseCanvas(m_gameCanvasEntityId, false); - gEnv->pLyShine->OnLoadScreenUnloaded(); + AZ::Interface::Get()->ReleaseCanvas(m_gameCanvasEntityId, false); + AZ::Interface::Get()->OnLoadScreenUnloaded(); } } @@ -198,8 +199,8 @@ namespace LyShine EBUS_EVENT_RESULT(canvasEntity, AZ::ComponentApplicationBus, FindEntity, m_levelCanvasEntityId); if (canvasEntity) { - gEnv->pLyShine->ReleaseCanvas(m_levelCanvasEntityId, false); - gEnv->pLyShine->OnLoadScreenUnloaded(); + AZ::Interface::Get()->ReleaseCanvas(m_levelCanvasEntityId, false); + AZ::Interface::Get()->OnLoadScreenUnloaded(); } } } @@ -234,7 +235,7 @@ namespace LyShine return AZ::EntityId(); } - AZ::EntityId canvasId = gEnv->pLyShine->LoadCanvas(path); + AZ::EntityId canvasId = AZ::Interface::Get()->LoadCanvas(path); AZ_Warning("LoadScreenComponent", canvasId.IsValid(), "Can't load canvas: %s", path.c_str()); if (!canvasId.IsValid()) { diff --git a/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp b/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp index 787797e462..7253935c2d 100644 --- a/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp +++ b/Gems/LyShine/Code/Source/LyShineSystemComponent.cpp @@ -384,16 +384,16 @@ namespace LyShine // When module is linked statically, we'll share the application's gEnv pointer. gEnv = system.GetGlobalEnvironment(); #endif - m_pLyShine = new CLyShine(gEnv->pSystem); - gEnv->pLyShine = m_pLyShine; + m_lyShine = AZStd::make_unique(); + AZ::Interface::Register(m_lyShine.get()); system.GetILevelSystem()->AddListener(this); BroadcastCursorImagePathname(); - if (gEnv->pLyShine) + if (AZ::Interface::Get()) { - gEnv->pLyShine->PostInit(); + AZ::Interface::Get()->PostInit(); } } @@ -402,18 +402,20 @@ namespace LyShine { system.GetILevelSystem()->RemoveListener(this); - gEnv->pLyShine = nullptr; - delete m_pLyShine; - m_pLyShine = nullptr; + if (m_lyShine) + { + AZ::Interface::Unregister(m_lyShine.get()); + m_lyShine.reset(); + } } //////////////////////////////////////////////////////////////////////// void LyShineSystemComponent::OnUnloadComplete([[maybe_unused]] const char* levelName) { // Perform level unload procedures for the LyShine UI system - if (gEnv && gEnv->pLyShine) + if (AZ::Interface::Get()) { - gEnv->pLyShine->OnLevelUnload(); + AZ::Interface::Get()->OnLevelUnload(); } } diff --git a/Gems/LyShine/Code/Source/LyShineSystemComponent.h b/Gems/LyShine/Code/Source/LyShineSystemComponent.h index d2cdcf6761..001ddd6fab 100644 --- a/Gems/LyShine/Code/Source/LyShineSystemComponent.h +++ b/Gems/LyShine/Code/Source/LyShineSystemComponent.h @@ -107,7 +107,7 @@ namespace LyShine protected: // data - CLyShine* m_pLyShine = nullptr; + AZStd::unique_ptr m_lyShine; AzFramework::SimpleAssetReference m_cursorImagePathname; diff --git a/Gems/LyShine/Code/Source/Script/UiCanvasLuaBus.cpp b/Gems/LyShine/Code/Source/Script/UiCanvasLuaBus.cpp index 4b0ff56241..679f13b333 100644 --- a/Gems/LyShine/Code/Source/Script/UiCanvasLuaBus.cpp +++ b/Gems/LyShine/Code/Source/Script/UiCanvasLuaBus.cpp @@ -114,7 +114,7 @@ AZ::EntityId UiCanvasLuaProxy::LoadCanvas(const char* canvasFilename) CryWarning(VALIDATOR_MODULE_SYSTEM, VALIDATOR_WARNING, "UiCanvasLuaProxy:LoadCanvas is deprecated. Please use UiCanvasManagerBus:LoadCanvas instead\n"); - return gEnv->pLyShine->LoadCanvas(canvasFilename); + return AZ::Interface::Get()->LoadCanvas(canvasFilename); } void UiCanvasLuaProxy::UnloadCanvas(AZ::EntityId canvasEntityId) @@ -130,7 +130,7 @@ void UiCanvasLuaProxy::UnloadCanvas(AZ::EntityId canvasEntityId) UiCanvasComponent* canvasComponent = canvasEntity->FindComponent(); if (canvasComponent) { - gEnv->pLyShine->ReleaseCanvas(canvasEntityId, false); + AZ::Interface::Get()->ReleaseCanvas(canvasEntityId, false); } else { diff --git a/Gems/LyShine/Code/Source/UiCanvasComponent.cpp b/Gems/LyShine/Code/Source/UiCanvasComponent.cpp index ea6e3681a3..0a7161ecb3 100644 --- a/Gems/LyShine/Code/Source/UiCanvasComponent.cpp +++ b/Gems/LyShine/Code/Source/UiCanvasComponent.cpp @@ -254,9 +254,9 @@ namespace UiRenderer* GetUiRendererForGame() { - if (gEnv && gEnv->pLyShine) + if (AZ::Interface::Get()) { - CLyShine* lyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); return lyShine->GetUiRenderer(); } return nullptr; @@ -264,9 +264,9 @@ namespace UiRenderer* GetUiRendererForEditor() { - if (gEnv && gEnv->pLyShine) + if (AZ::Interface::Get()) { - CLyShine* lyShine = static_cast(gEnv->pLyShine); + CLyShine* lyShine = static_cast(AZ::Interface::Get()); return lyShine->GetUiRendererForEditor(); } return nullptr; diff --git a/Gems/LyShine/Code/Source/UiImageComponent.cpp b/Gems/LyShine/Code/Source/UiImageComponent.cpp index a8b4210d6b..d412695ac3 100644 --- a/Gems/LyShine/Code/Source/UiImageComponent.cpp +++ b/Gems/LyShine/Code/Source/UiImageComponent.cpp @@ -588,7 +588,7 @@ void UiImageComponent::SetSpritePathname(AZStd::string spritePath) //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiImageComponent::SetSpritePathnameIfExists(AZStd::string spritePath) { - if (gEnv->pLyShine->DoesSpriteTextureAssetExist(spritePath)) + if (AZ::Interface::Get()->DoesSpriteTextureAssetExist(spritePath)) { SetSpritePathname(spritePath); return true; @@ -1179,7 +1179,7 @@ void UiImageComponent::Init() // If this is called from RC.exe for example these pointers will not be set. In that case // we only need to be able to load, init and save the component. It will never be // activated. - if (!(gEnv && gEnv->pLyShine)) + if (!AZ::Interface::Get()) { return; } @@ -1192,14 +1192,14 @@ void UiImageComponent::Init() { if (!m_spritePathname.GetAssetPath().empty()) { - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } } else if (m_spriteType == UiImageInterface::SpriteType::RenderTarget) { if (!m_renderTargetName.empty()) { - m_sprite = gEnv->pLyShine->CreateSprite(m_renderTargetName.c_str()); + m_sprite = AZ::Interface::Get()->CreateSprite(m_renderTargetName.c_str()); } } else @@ -2527,7 +2527,7 @@ void UiImageComponent::OnSpritePathnameChange() if (!m_spritePathname.GetAssetPath().empty()) { // Load the new texture. - newSprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + newSprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } // if listening for notifications from a current sprite then disconnect @@ -2557,7 +2557,7 @@ void UiImageComponent::OnSpriteRenderTargetNameChange() if (!m_renderTargetName.empty()) { - newSprite = gEnv->pLyShine->CreateSprite(m_renderTargetName.c_str()); + newSprite = AZ::Interface::Get()->CreateSprite(m_renderTargetName.c_str()); } SAFE_RELEASE(m_sprite); diff --git a/Gems/LyShine/Code/Source/UiImageSequenceComponent.cpp b/Gems/LyShine/Code/Source/UiImageSequenceComponent.cpp index 3559dc6306..3981777f2d 100644 --- a/Gems/LyShine/Code/Source/UiImageSequenceComponent.cpp +++ b/Gems/LyShine/Code/Source/UiImageSequenceComponent.cpp @@ -52,7 +52,7 @@ namespace spriteList.reserve(imageList.size()); for (auto& textureAssetRef : imageList) { - ISprite* sprite = gEnv->pLyShine->LoadSprite(textureAssetRef.GetAssetPath().c_str()); + ISprite* sprite = AZ::Interface::Get()->LoadSprite(textureAssetRef.GetAssetPath().c_str()); if (sprite) { spriteList.push_back(sprite); @@ -383,7 +383,7 @@ void UiImageSequenceComponent::Init() // If this is called from RC.exe for example these pointers will not be set. In that case // we only need to be able to load, init and save the component. It will never be // activated. - if (!(gEnv && gEnv->pLyShine)) + if (!AZ::Interface::Get()) { return; } diff --git a/Gems/LyShine/Code/Source/UiInteractableState.cpp b/Gems/LyShine/Code/Source/UiInteractableState.cpp index d2c09201c6..18bacb3a9f 100644 --- a/Gems/LyShine/Code/Source/UiInteractableState.cpp +++ b/Gems/LyShine/Code/Source/UiInteractableState.cpp @@ -274,7 +274,7 @@ UiInteractableStateSprite::UiInteractableStateSprite(AZ::EntityId target, const if (!m_spritePathname.GetAssetPath().empty()) { - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } } @@ -297,7 +297,7 @@ void UiInteractableStateSprite::Init(AZ::EntityId interactableEntityId) // If this is called from RC.exe for example these pointers will not be set. In that case // we only need to be able to load, init and save the component. It will never be // activated. - if (!(gEnv && gEnv->pLyShine)) + if (!AZ::Interface::Get()) { return; } @@ -306,7 +306,7 @@ void UiInteractableStateSprite::Init(AZ::EntityId interactableEntityId) // are not loaded then load them if (!m_sprite && !m_spritePathname.GetAssetPath().empty()) { - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } if (!m_sprite) @@ -366,7 +366,7 @@ void UiInteractableStateSprite::OnSpritePathnameChange() if (!m_spritePathname.GetAssetPath().empty()) { // Load the new texture. - newSprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + newSprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } SAFE_RELEASE(m_sprite); diff --git a/Gems/LyShine/Code/Source/UiParticleEmitterComponent.cpp b/Gems/LyShine/Code/Source/UiParticleEmitterComponent.cpp index b999480940..b9f986c14a 100644 --- a/Gems/LyShine/Code/Source/UiParticleEmitterComponent.cpp +++ b/Gems/LyShine/Code/Source/UiParticleEmitterComponent.cpp @@ -1429,14 +1429,14 @@ void UiParticleEmitterComponent::Init() // If this is called from RC.exe for example these pointers will not be set. In that case // we only need to be able to load, init and save the component. It will never be // activated. - if (!(gEnv && gEnv->pLyShine)) + if (!AZ::Interface::Get()) { return; } if (!m_sprite && !m_spritePathname.GetAssetPath().empty()) { - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } m_currentAspectRatio = m_particleSize.GetX() / m_particleSize.GetY(); @@ -1927,7 +1927,7 @@ void UiParticleEmitterComponent::OnSpritePathnameChange() if (!m_spritePathname.GetAssetPath().empty()) { // Load the new texture. - newSprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + newSprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } SAFE_RELEASE(m_sprite); diff --git a/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp b/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp index 933cca424a..e186e1158d 100644 --- a/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp +++ b/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp @@ -8,6 +8,7 @@ #include "UiCanvasAssetRefComponent.h" #include #include +#include #include #include #include @@ -106,11 +107,11 @@ AZ::EntityId UiCanvasAssetRefComponent::LoadCanvas() // Check if we already have a referenced UI canvas, if so release it if (m_canvasEntityId.IsValid()) { - gEnv->pLyShine->ReleaseCanvasDeferred(m_canvasEntityId); + AZ::Interface::Get()->ReleaseCanvasDeferred(m_canvasEntityId); m_canvasEntityId.SetInvalid(); } - m_canvasEntityId = gEnv->pLyShine->LoadCanvas(canvasPath.c_str()); + m_canvasEntityId = AZ::Interface::Get()->LoadCanvas(canvasPath.c_str()); EBUS_EVENT_ID(GetEntityId(), UiCanvasAssetRefNotificationBus, OnCanvasLoadedIntoEntity, m_canvasEntityId); EBUS_EVENT_ID(GetEntityId(), UiCanvasRefNotificationBus, OnCanvasRefChanged, GetEntityId(), m_canvasEntityId); @@ -124,7 +125,7 @@ void UiCanvasAssetRefComponent::UnloadCanvas() { if (m_canvasEntityId.IsValid()) { - gEnv->pLyShine->ReleaseCanvasDeferred(m_canvasEntityId); + AZ::Interface::Get()->ReleaseCanvasDeferred(m_canvasEntityId); m_canvasEntityId.SetInvalid(); EBUS_EVENT_ID(GetEntityId(), UiCanvasRefNotificationBus, OnCanvasRefChanged, GetEntityId(), m_canvasEntityId); @@ -243,7 +244,7 @@ void UiCanvasAssetRefComponent::Deactivate() { if (m_canvasEntityId.IsValid()) { - gEnv->pLyShine->ReleaseCanvasDeferred(m_canvasEntityId); + AZ::Interface::Get()->ReleaseCanvasDeferred(m_canvasEntityId); m_canvasEntityId.SetInvalid(); } diff --git a/Gems/LyShine/Code/Tests/UiTooltipComponentTest.cpp b/Gems/LyShine/Code/Tests/UiTooltipComponentTest.cpp index 8a3d9f2af6..652cec3c0f 100644 --- a/Gems/LyShine/Code/Tests/UiTooltipComponentTest.cpp +++ b/Gems/LyShine/Code/Tests/UiTooltipComponentTest.cpp @@ -144,7 +144,6 @@ namespace UnitTest SSystemGlobalEnvironment env; SSystemGlobalEnvironment* prevEnv = gEnv; gEnv = &env; - gEnv->pLyShine = nullptr; auto [uiCanvasComponent, uiTooltipDisplayComponent, uiTooltipComponent] = CreateUiCanvasWithTooltip(); uiTooltipDisplayComponent->SetTriggerMode(UiTooltipDisplayInterface::TriggerMode::OnHover); @@ -170,7 +169,6 @@ namespace UnitTest SSystemGlobalEnvironment env; SSystemGlobalEnvironment* prevEnv = gEnv; gEnv = &env; - gEnv->pLyShine = nullptr; auto [uiCanvasComponent, uiTooltipDisplayComponent, uiTooltipComponent] = CreateUiCanvasWithTooltip(); uiTooltipDisplayComponent->SetTriggerMode(UiTooltipDisplayInterface::TriggerMode::OnHover); @@ -194,7 +192,6 @@ namespace UnitTest SSystemGlobalEnvironment env; SSystemGlobalEnvironment* prevEnv = gEnv; gEnv = &env; - gEnv->pLyShine = nullptr; auto [uiCanvasComponent, uiTooltipDisplayComponent, uiTooltipComponent] = CreateUiCanvasWithTooltip(); uiTooltipDisplayComponent->SetTriggerMode(UiTooltipDisplayInterface::TriggerMode::OnPress); @@ -218,7 +215,6 @@ namespace UnitTest SSystemGlobalEnvironment env; SSystemGlobalEnvironment* prevEnv = gEnv; gEnv = &env; - gEnv->pLyShine = nullptr; auto [uiCanvasComponent, uiTooltipDisplayComponent, uiTooltipComponent] = CreateUiCanvasWithTooltip(); uiTooltipDisplayComponent->SetTriggerMode(UiTooltipDisplayInterface::TriggerMode::OnPress); @@ -242,7 +238,6 @@ namespace UnitTest SSystemGlobalEnvironment env; SSystemGlobalEnvironment* prevEnv = gEnv; gEnv = &env; - gEnv->pLyShine = nullptr; auto [uiCanvasComponent, uiTooltipDisplayComponent, uiTooltipComponent] = CreateUiCanvasWithTooltip(); uiTooltipDisplayComponent->SetTriggerMode(UiTooltipDisplayInterface::TriggerMode::OnClick); diff --git a/Gems/LyShineExamples/Code/Source/LyShineExamplesCppExample.cpp b/Gems/LyShineExamples/Code/Source/LyShineExamplesCppExample.cpp index f2da79fa99..7619764fbc 100644 --- a/Gems/LyShineExamples/Code/Source/LyShineExamplesCppExample.cpp +++ b/Gems/LyShineExamples/Code/Source/LyShineExamplesCppExample.cpp @@ -47,7 +47,7 @@ namespace LyShineExamples // Remove the existing example canvas if it exists DestroyCanvas(); - AZ::EntityId canvasEntityId = gEnv->pLyShine->CreateCanvas(); + AZ::EntityId canvasEntityId = AZ::Interface::Get()->CreateCanvas(); if (!canvasEntityId.IsValid()) { return; @@ -90,7 +90,7 @@ namespace LyShineExamples m_healthBar.SetInvalid(); - gEnv->pLyShine->ReleaseCanvas(m_canvasId, false); + AZ::Interface::Get()->ReleaseCanvas(m_canvasId, false); m_canvasId.SetInvalid(); } } diff --git a/Gems/LyShineExamples/Code/Source/UiCustomImageComponent.cpp b/Gems/LyShineExamples/Code/Source/UiCustomImageComponent.cpp index e8b0e4370a..c228176b4f 100644 --- a/Gems/LyShineExamples/Code/Source/UiCustomImageComponent.cpp +++ b/Gems/LyShineExamples/Code/Source/UiCustomImageComponent.cpp @@ -284,7 +284,7 @@ namespace LyShineExamples // If this is called from RC.exe for example these pointers will not be set. In that case // we only need to be able to load, init and save the component. It will never be // activated. - if (!(gEnv && gEnv->pLyShine)) + if (!AZ::Interface::Get()) { return; } @@ -294,7 +294,7 @@ namespace LyShineExamples { if (!m_spritePathname.GetAssetPath().empty()) { - m_sprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + m_sprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } } @@ -399,7 +399,7 @@ namespace LyShineExamples if (!m_spritePathname.GetAssetPath().empty()) { // Load the new texture. - newSprite = gEnv->pLyShine->LoadSprite(m_spritePathname.GetAssetPath().c_str()); + newSprite = AZ::Interface::Get()->LoadSprite(m_spritePathname.GetAssetPath().c_str()); } SAFE_RELEASE(m_sprite); diff --git a/Gems/MessagePopup/Code/Source/LyShineMessagePopup.cpp b/Gems/MessagePopup/Code/Source/LyShineMessagePopup.cpp index 40fed3448b..de54bef4eb 100644 --- a/Gems/MessagePopup/Code/Source/LyShineMessagePopup.cpp +++ b/Gems/MessagePopup/Code/Source/LyShineMessagePopup.cpp @@ -127,21 +127,21 @@ namespace MessagePopup switch (_buttons) { case EPopupButtons::EPopupButtons_NoButtons: - canvasEntityId = gEnv->pLyShine->LoadCanvas("@products@/ui/canvases/defaultmessagepopup.uicanvas"); + canvasEntityId = AZ::Interface::Get()->LoadCanvas("@products@/ui/canvases/defaultmessagepopup.uicanvas"); break; case EPopupButtons::EPopupButtons_Confirm: - canvasEntityId = gEnv->pLyShine->LoadCanvas("@products@/ui/canvases/defaultmessagepopup_confirm.uicanvas"); + canvasEntityId = AZ::Interface::Get()->LoadCanvas("@products@/ui/canvases/defaultmessagepopup_confirm.uicanvas"); isNavigationSupported = true; break; case EPopupButtons::EPopupButtons_YesNo: - canvasEntityId = gEnv->pLyShine->LoadCanvas("@products@/ui/canvases/defaultmessagepopup_yesno.uicanvas"); + canvasEntityId = AZ::Interface::Get()->LoadCanvas("@products@/ui/canvases/defaultmessagepopup_yesno.uicanvas"); isNavigationSupported = true; break; } } else if (_kind == EPopupKind_Toaster) { - canvasEntityId = gEnv->pLyShine->LoadCanvas("@products@/ui/canvases/toaster.uicanvas"); + canvasEntityId = AZ::Interface::Get()->LoadCanvas("@products@/ui/canvases/toaster.uicanvas"); } if (canvasEntityId.IsValid()) @@ -183,7 +183,7 @@ namespace MessagePopup { // get the canvas ID in the clientdata LyShine::CanvasId canvasId = *((LyShine::CanvasId*)&_popupInfo.m_clientData); - AZ::EntityId canvasEntityId = gEnv->pLyShine->FindCanvasById(canvasId); + AZ::EntityId canvasEntityId = AZ::Interface::Get()->FindCanvasById(canvasId); if (canvasEntityId.IsValid()) { // Hide the cursor if it was shown in LyShineMessagePopup::OnShowPopup @@ -197,7 +197,7 @@ namespace MessagePopup // Disable the popup EBUS_EVENT_ID(canvasEntityId, UiCanvasBus, SetEnabled, false); - gEnv->pLyShine->ReleaseCanvas(canvasEntityId, false); + AZ::Interface::Get()->ReleaseCanvas(canvasEntityId, false); UiCanvasNotificationBus::MultiHandler::BusDisconnect(canvasEntityId); m_activePopupIdsByCanvasId.erase(canvasEntityId); From a145d3c82fa3c5a71dc0545708518d7bd7769d53 Mon Sep 17 00:00:00 2001 From: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> Date: Fri, 21 Jan 2022 15:07:27 -0800 Subject: [PATCH 25/37] Issues/install missing assets (#7081) * Fixes for missing assets in the install folder Signed-off-by: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> * delayed expansion to try to fix project_engineinstall_profile Signed-off-by: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> * Improvements to build scripts to solve the project build from the install folder Signed-off-by: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com> --- CMakeLists.txt | 4 ++++ cmake/Platform/Common/Install_common.cmake | 8 +++++--- .../build/Platform/Windows/build_windows.cmd | 11 ++++++----- .../build/Platform/Windows/env_windows.cmd | 19 ++++++++++++------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c81581fe0f..efcc866195 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,10 @@ endfunction() # Add the projects first so the Launcher can find them include(cmake/Projects.cmake) +if(LY_EXTERNAL_SUBDIRS) + set_property(GLOBAL APPEND PROPERTY LY_EXTERNAL_SUBDIRS ${LY_EXTERNAL_SUBDIRS}) +endif() + if(NOT INSTALLED_ENGINE) # Add external subdirectories listed in the engine.json. LY_EXTERNAL_SUBDIRS is a cache variable so the user can add extra # external subdirectories. This should go before adding the rest of the targets so the targets are availbe to the launcher. diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index 46130f1345..e42664c107 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -445,9 +445,10 @@ function(ly_setup_cmake_install) ) endforeach() - # Transform the LY_EXTERNAL_SUBDIRS list into a json array + # Transform the LY_EXTERNAL_SUBDIRS global property list into a json array set(indent " ") - foreach(external_subdir ${LY_EXTERNAL_SUBDIRS}) + get_property(external_subdirs GLOBAL PROPERTY LY_EXTERNAL_SUBDIRS) + foreach(external_subdir ${external_subdirs}) cmake_path(RELATIVE_PATH external_subdir BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE engine_rel_external_subdir) list(APPEND relative_external_subdirs "\"${engine_rel_external_subdir}\"") endforeach() @@ -639,7 +640,8 @@ function(ly_setup_assets) # the install layout from the root directory. Such as /Cache. # This is also done to avoid globbing thousands of files in subdirectories that shouldn't # be processed. - foreach(gem_candidate_dir IN LISTS LY_EXTERNAL_SUBDIRS LY_PROJECTS) + get_property(external_subdirs GLOBAL PROPERTY LY_EXTERNAL_SUBDIRS) + foreach(gem_candidate_dir IN LISTS external_subdirs LY_PROJECTS) file(REAL_PATH ${gem_candidate_dir} gem_candidate_dir BASE_DIRECTORY ${LY_ROOT_FOLDER}) # Don't recurse immediately in order to exclude transient source artifacts file(GLOB diff --git a/scripts/build/Platform/Windows/build_windows.cmd b/scripts/build/Platform/Windows/build_windows.cmd index 798550370f..c77c262692 100644 --- a/scripts/build/Platform/Windows/build_windows.cmd +++ b/scripts/build/Platform/Windows/build_windows.cmd @@ -9,6 +9,7 @@ REM SETLOCAL EnableDelayedExpansion CALL "%~dp0env_windows.cmd" +IF NOT %ERRORLEVEL%==0 EXIT /b 1 IF NOT EXIST "%OUTPUT_DIRECTORY%" ( MKDIR %OUTPUT_DIRECTORY%. @@ -21,7 +22,7 @@ ECHO [ci_build] cmake --version cmake --version IF ERRORLEVEL 1 ( ECHO [ci_build] CMAKE not found! - exit /b 1 + EXIT /b 1 ) REM Compute half the amount of processors so some jobs can run @@ -47,14 +48,14 @@ IF NOT EXIST CMakeCache.txt ( ) ) IF DEFINED RUN_CONFIGURE ( - ECHO [ci_build] %CONFIGURE_CMD% - %CONFIGURE_CMD% + call ECHO [ci_build] %CONFIGURE_CMD% + call %CONFIGURE_CMD% IF NOT !ERRORLEVEL!==0 GOTO :error ECHO !CONFIGURE_CMD!> %LAST_CONFIGURE_CMD_FILE% ) -ECHO [ci_build] cmake --build . --target %CMAKE_TARGET% --config %CONFIGURATION% %CMAKE_BUILD_ARGS% -- %CMAKE_NATIVE_BUILD_ARGS% -cmake --build . --target %CMAKE_TARGET% --config %CONFIGURATION% %CMAKE_BUILD_ARGS% -- %CMAKE_NATIVE_BUILD_ARGS% +call ECHO [ci_build] cmake --build . --target %CMAKE_TARGET% --config %CONFIGURATION% %CMAKE_BUILD_ARGS% -- %CMAKE_NATIVE_BUILD_ARGS% +call cmake --build . --target %CMAKE_TARGET% --config %CONFIGURATION% %CMAKE_BUILD_ARGS% -- %CMAKE_NATIVE_BUILD_ARGS% IF NOT %ERRORLEVEL%==0 GOTO :error POPD diff --git a/scripts/build/Platform/Windows/env_windows.cmd b/scripts/build/Platform/Windows/env_windows.cmd index 78b5ca6c1a..c6f46ef575 100644 --- a/scripts/build/Platform/Windows/env_windows.cmd +++ b/scripts/build/Platform/Windows/env_windows.cmd @@ -13,17 +13,21 @@ SETLOCAL EnableExtensions EnableDelayedExpansion where /Q cmake IF NOT %ERRORLEVEL%==0 ( ECHO [ci_build] CMake not found - GOTO :error -) - -IF NOT "%COMMAND_CWD%"=="" ( - ECHO [ci_build] Changing CWD to %COMMAND_CWD% - CD %COMMAND_CWD% + GOTO :errorlocal ) REM Ending the local environment to be able to propagate the TMP/TEMP variables to the calling script ENDLOCAL +IF NOT "%COMMAND_CWD%"=="" ( + call ECHO [ci_build] Changing CWD to %COMMAND_CWD% + call CD %COMMAND_CWD% + IF ERRORLEVEL 1 ( + ECHO [ci_build] Failed to change directory to %COMMAND_CWD% + GOTO :error + ) +) + REM Jenkins does not defined TMP IF "%TMP%"=="" ( IF "%WORKSPACE%"=="" ( @@ -41,6 +45,7 @@ IF "%TMP%"=="" ( EXIT /b 0 -:error +:errorlocal ENDLOCAL +:error EXIT /b 1 \ No newline at end of file From dbd6ddbc1c62db227344407a1e51421380adde1c Mon Sep 17 00:00:00 2001 From: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:05:19 -0600 Subject: [PATCH 26/37] Optimized Gradient Previewer (#7074) * Faster method with older one commented out for profiling. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Removed commented-out code Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Addressed PR feedback, fixed subtle bug in interlace math. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fixed unrelated bug in terrain cmake file. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> * Fix linux compile error. Signed-off-by: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> --- .../Editor/EditorGradientPreviewRenderer.h | 202 +++++++++++------- Gems/Terrain/Code/CMakeLists.txt | 1 - 2 files changed, 126 insertions(+), 77 deletions(-) diff --git a/Gems/GradientSignal/Code/Include/GradientSignal/Editor/EditorGradientPreviewRenderer.h b/Gems/GradientSignal/Code/Include/GradientSignal/Editor/EditorGradientPreviewRenderer.h index 2790a668a3..db39431ba1 100644 --- a/Gems/GradientSignal/Code/Include/GradientSignal/Editor/EditorGradientPreviewRenderer.h +++ b/Gems/GradientSignal/Code/Include/GradientSignal/Editor/EditorGradientPreviewRenderer.h @@ -25,7 +25,6 @@ #include #include - namespace GradientSignal { //! EditorGradientPreviewUpdateJob offloads the creation of a gradient preview image to another thread. @@ -223,7 +222,7 @@ namespace GradientSignal // If you ever want to make this use a smaller number of passes, set this value to any even number. // A value of 0 is the same as "non-interlaced", 6 would exactly use the Adam7 algorithm, etc. // It's currently clamping to 30 as a somewhat aribtrary choice. - const uint64_t maxFinalInterlacingPass = 30; + const int64_t maxFinalInterlacingPass = 30; m_finalInterlacingPass = AZ::GetMin(m_finalInterlacingPass, maxFinalInterlacingPass); // Finally, lock our mutex, modify our status variables, and start the Job. @@ -238,6 +237,8 @@ namespace GradientSignal //! Process runs exactly once for each time Start() is called on a Job, and processes on a Job worker thread. void Process() override { + AZ_PROFILE_FUNCTION(Entity); + // Guard against the case that we're trying to cancel even before we've started to run. if (!m_shouldCancel) { @@ -247,88 +248,136 @@ namespace GradientSignal // This is the "striding value". When walking directly through our preview image bits() buffer, there might be // extra pad bytes for each line due to alignment. We use this to make sure we start writing each line at the right byte offset. - const uint64_t imageBytesPerLine = m_previewImage->bytesPerLine(); + const int64_t imageBytesPerLine = m_previewImage->bytesPerLine(); - // The following are all used for calculating our interlaced pixel updates. + // Keep track of the total number of pixels that we intend to process. For easy interlacing calculations, we always use + // square power-of-two conceptual images, but we'll skip any pixels that fall outside of our actual image bounds. + const int64_t totalPixels = (m_imageBoundsPowerOfTwo * m_imageBoundsPowerOfTwo); - // The current interlace pass that we're on. - int64_t curPass = 0; - // The index of the first pixel for this pass. This is used to calculate the relative pixel index per pass. - uint64_t firstPixelPerPass = 0; - // Total number of pixels that we'll process per pass. After the first two passes, the amount doubles per pass till we reach 100%. - uint64_t totalPixelsPerPass = (m_imageBoundsPowerOfTwo * m_imageBoundsPowerOfTwo) / (1LL << (m_finalInterlacingPass - curPass)); - // The general interlace formulas need a multiplier and an offset for x and y to apply to each relative pixel index. - // The pixel multipliers start high and reduce on each pass to increase the pixel density per pass. - // The pixel offsets alternate between 0 and a reducing number because we start on aligned grids, then fill in the midpoints - // of the grids on every other pass. - uint64_t xPixelMult = 1LL << (m_finalInterlacingPass / 2); - uint64_t xPixelOffset = 0; - uint64_t yPixelMult = 1LL << (m_finalInterlacingPass / 2); - uint64_t yPixelOffset = 0; + // Preallocate buffers for our gradient lookup positions, our gradient output values, and the corresponding pixel buffer + // index to store the value into. These allow us to fetch gradient values in bulk, which is much faster than fetching them + // individually. The max size we'll need is for our last interacing pass which requests 50% of our total pixels (as + // described further below), so that's what we will preallocate. + AZStd::vector gradientLookupPositions(totalPixels / 2); + AZStd::vector gradientValues(totalPixels / 2); + AZStd::vector pixelBufferIndex(totalPixels / 2); - // The heart of the processing - loop through each pixel using interlaced indexing, get the gradient value, and write it into - // the pixel buffer. On each pixel, we also check to see if the main thread requested a cancel so that we can early-out. - // The loop itself runs through the full square power-of-two bounds that encapsulates our image so that we can perform our - // interlaced indexing easily, but we skip processing any pixel that falls outside the actual image bounds. - for (uint64_t curPixel = 0; (!m_shouldCancel) && (curPixel < (m_imageBoundsPowerOfTwo * m_imageBoundsPowerOfTwo)); curPixel++) + // The following loop uses a variant of the Adam7 interlacing algorithm that's been generalized to work for N passes, + // instead of exactly 7 passes. The first two passes fill in 1 pixel each, and then each subsequent pass doubles the + // number of pixels it fills in, until the last pass fills in 50%. + // Note that m_finalInteracingPass contains the value of the final pass to process, not the total number of passes. + // On each pass, we'll also early-out if the main thread requested a cancellation. + for (int64_t curPass = 0; (!m_shouldCancel) && (curPass <= m_finalInterlacingPass); curPass++) { - // Check to see if we've finished the pixels for this pass and need to move on to the next pass. - if (curPixel >= (firstPixelPerPass + totalPixelsPerPass)) + gradientLookupPositions.clear(); + pixelBufferIndex.clear(); + gradientValues.clear(); + + // The general interlace formulas need a multiplier and an offset for x and y to apply to each relative pixel index. + // + // The first 3 passes are a little different than the others because they establish the base pattern: + // 1 . . . 2 . . . + // . . . . . . . . + // . . . . . . . . + // . . . . . . . . + // 3 . . . 3 . . . + // . . . . . . . . + // . . . . . . . . + // . . . . . . . . + // + // Every 2 passes from then on do the same thing, with shrinking grids. One pass fills in the grid X midpoints on the + // lines that were already processed, and the second pass fills in all the equivalent points on the Y grid midpoints + // x . 4 . x . 4 . + // . . . . . . . . + // 5 . 5 . 5 . 5 . + // . . . . . . . . + // x . 4 . x . 4 . + // . . . . . . . . + // 5 . 5 . 5 . 5 . + // . . . . . . . . + // + // x 6 x 6 x 6 x 6 + // 7 7 7 7 7 7 7 7 + // x 6 x 6 x 6 x 6 + // 7 7 7 7 7 7 7 7 + // x 6 x 6 x 6 x 6 + // 7 7 7 7 7 7 7 7 + // x 6 x 6 x 6 x 6 + // 7 7 7 7 7 7 7 7 + // + // The total number of pixels processed per pass starts at 1 pixel each for the first two passes, then doubles per + // pass till we reach 50% in the last pass, since all the other passes before it will have covered the other 50%. + // Ex: 7 passes will do N/64, N/64, N/32, N/16, N/8, N/4, N/2 pixels per pass. + + + // For X, we want our starting pixel offset to alternate between 0 and a decreasing power of 2 on every pass, and + // our stride to decrease by a power of 2 every two passes, ending with an offset of 0 and a stride of 1 on the last + // pass. + const int64_t xOffsetShifter = AZ::GetMin(m_finalInterlacingPass - curPass, m_finalInterlacingPass - 1); + const int64_t xPixelOffset = (curPass % 2) * (1LL << (xOffsetShifter / 2)); + const int64_t xPixelStride = 1LL << ((xOffsetShifter + 1) / 2); + + // For Y, we want our starting pixel offset and our stride to behave the same as X, except that we hold our starting + // offset and stride for one additional pass which is what causes the first 3 passes to behave differently than the + // rest. The pass offset between X and Y is also what causes the pattern to keep filling in pixels and lines that + // haven't already been processed. + const int64_t laggingPass = AZ::GetMax(curPass - 1, 0); + const int64_t yOffsetShifter = AZ::GetMin(m_finalInterlacingPass - laggingPass, m_finalInterlacingPass - 1); + const int64_t yPixelOffset = (laggingPass % 2) * (1LL << (yOffsetShifter / 2)); + const int64_t yPixelStride = 1LL << ((yOffsetShifter + 1) / 2); + + // First, we loop and fill in all the gradientLookupPositions and pixelBufferIndex values for any pixels that don't + // get culled out. We're using a power of two for calculating our interlacing offsets and strides, but we don't need + // to actually process any of those pixels that fall outside our image bounds, so we end our loops at the bounds. + for (int64_t y = yPixelOffset; y < m_imageBoundsY; y += yPixelStride) { - curPass++; - - // Adjust our interlacing formula adjustments on each pass. These will cause us to process an increasing - // number of pixels at a higher density on each pass, interleaving in a way that ensures each pixel is only - // processed once at the end. - yPixelMult = xPixelMult; - yPixelOffset = xPixelOffset; - xPixelMult = 1LL << ((m_finalInterlacingPass - curPass + 1) / 2); - xPixelOffset = (curPass % 2) * (1LL << ((m_finalInterlacingPass - curPass) / 2)); - - firstPixelPerPass += totalPixelsPerPass; - totalPixelsPerPass = (m_imageBoundsPowerOfTwo * m_imageBoundsPowerOfTwo) / (1LL << (m_finalInterlacingPass - curPass + 1)); - } - - // Here's where interlacing happens. If this were non-interlaced, we'd simply have the following: - // x = curPixel % m_imageBoundsPowerOfTwo - // y = curPixel / m_imageBoundsPowerOfTwo - uint64_t adjustedPixel = curPixel - firstPixelPerPass; - uint64_t x = ((adjustedPixel * xPixelMult) + xPixelOffset) % m_imageBoundsPowerOfTwo; - uint64_t y = ((((adjustedPixel * xPixelMult) + xPixelOffset) / m_imageBoundsPowerOfTwo) * yPixelMult) + yPixelOffset; - - // Since we're using a power of two for calculating our interlacing, it's possible to get pixel offsets beyond the bounds - // of our actual image. We just skip those and continue on to the next pixel. - if ((x >= m_imageBoundsX) || (y >= m_imageBoundsY)) - { - continue; - } - - // Now that we've calculated the pixel position, update it with the gradient value. - { - // Invert world y to match axis. (We use "imageBoundsY- 1" to invert because our loop doesn't go all the way to imageBoundsY) - AZ::Vector3 uvw(static_cast(x), static_cast((m_imageBoundsY - 1) - y), 0.0f); - - GradientSampleParams sampleParams; - sampleParams.m_position = m_previewBoundsStart + (uvw * m_pixelToBoundsScale) + m_scaledTexelOffset; - - bool inBounds = true; - if (m_constrainToShape) + for (int64_t x = xPixelOffset; x < m_imageBoundsX; x += xPixelStride) { - LmbrCentral::ShapeComponentRequestsBus::EventResult(inBounds, m_previewEntityId, &LmbrCentral::ShapeComponentRequestsBus::Events::IsPointInside, sampleParams.m_position); - } + // Map the pixel coordinate back into world coordinates for the shape and gradient queries. Note that we + // invert world y to match the world axis. (We use "imageBoundsY- 1" to invert because our loop doesn't go all + // the way to imageBoundsY) + AZ::Vector3 uvw(static_cast(x), static_cast((m_imageBoundsY - 1) - y), 0.0f); + AZ::Vector3 position = m_previewBoundsStart + (uvw * m_pixelToBoundsScale) + m_scaledTexelOffset; - float sample = inBounds ? m_sampler.GetValue(sampleParams) : 0.0f; + // If our preview is only drawing what appears inside the given shape, check to see if the pixel should be + // drawn. + bool inBounds = true; + if (m_constrainToShape) + { + LmbrCentral::ShapeComponentRequestsBus::EventResult( + inBounds, m_previewEntityId, &LmbrCentral::ShapeComponentRequestsBus::Events::IsPointInside, position); + } + + // If we're drawing this pixel, push it into our buffer of lookup positions. + if (inBounds) + { + gradientLookupPositions.emplace_back(position); + pixelBufferIndex.emplace_back(((m_centeringOffsetY + y) * imageBytesPerLine) + (m_centeringOffsetX + x)); + } + } + } + + // Resize our output buffer to match our input buffer and query for all the gradient values at once. + gradientValues.resize(gradientLookupPositions.size()); + m_sampler.GetValues(gradientLookupPositions, gradientValues); + + // For each output value, run it through a filter if we were given one, then store it in the pixel buffer. + for (size_t index = 0; index < gradientLookupPositions.size(); index++) + { + float sample = gradientValues[index]; if (m_filterFunc) { + GradientSampleParams sampleParams; + sampleParams.m_position = gradientLookupPositions[index]; sample = m_filterFunc(sample, sampleParams); } - buffer[((m_centeringOffsetY + y) * imageBytesPerLine) + (m_centeringOffsetX + x)] = static_cast(sample * 255); - } + buffer[pixelBufferIndex[index]] = static_cast(sample * 255); - // Notify the main thread via atomic bool that the image has changed by at least one pixel. - m_refreshUI = true; + // Notify the main thread via atomic bool that the image has changed by at least one pixel. + m_refreshUI = true; + } } } @@ -356,15 +405,15 @@ namespace GradientSignal AZ::EntityId m_previewEntityId; // Values calculated during preview setup that we'll use during processing - uint64_t m_imageBoundsX = 0; - uint64_t m_imageBoundsY = 0; - uint64_t m_centeringOffsetX = 0; - uint64_t m_centeringOffsetY = 0; + int64_t m_imageBoundsX = 0; + int64_t m_imageBoundsY = 0; + int64_t m_centeringOffsetX = 0; + int64_t m_centeringOffsetY = 0; AZ::Vector3 m_previewBoundsStart; AZ::Vector3 m_pixelToBoundsScale; AZ::Vector3 m_scaledTexelOffset; - uint64_t m_imageBoundsPowerOfTwo = 1; - uint64_t m_finalInterlacingPass = 0; + int64_t m_imageBoundsPowerOfTwo = 1; + int64_t m_finalInterlacingPass = 0; // Communication / synchronization mechanisms between the different threads AZStd::mutex m_previewMutex; @@ -456,3 +505,4 @@ namespace GradientSignal EditorGradientPreviewUpdateJob* m_updateJob = nullptr; }; } //namespace GradientSignal + diff --git a/Gems/Terrain/Code/CMakeLists.txt b/Gems/Terrain/Code/CMakeLists.txt index 69a1bef3c0..44e33b7008 100644 --- a/Gems/Terrain/Code/CMakeLists.txt +++ b/Gems/Terrain/Code/CMakeLists.txt @@ -98,7 +98,6 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) NAME Terrain.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem FILES_CMAKE - terrain_files.cmake terrain_tests_files.cmake INCLUDE_DIRECTORIES PRIVATE From dd7706f9feec5370a8d6b5e9b2565c13421016e9 Mon Sep 17 00:00:00 2001 From: chiyenteng <82238204+chiyenteng@users.noreply.github.com> Date: Fri, 21 Jan 2022 16:23:31 -0800 Subject: [PATCH 27/37] Fire OnEditorEntityCreated notification in SandboxIntegrationManager::CreateNewEntityAtPosition (#7079) * Ensure to fire OnEditorEntityCreated notification in SandboxIntegrationManager::CreateNewEntityAtPosition Signed-off-by: chiyenteng <82238204+chiyenteng@users.noreply.github.com> * Ensure to fire OnEditorEntityCreated notification in SandboxIntegrationManager::CreateNewEntityAtPosition Signed-off-by: chiyenteng <82238204+chiyenteng@users.noreply.github.com> * convert test AreaNodes_DependentComponentsAdded to use prefab system Signed-off-by: chiyenteng <82238204+chiyenteng@users.noreply.github.com> * Prevent SetupEditorEntity being called twice Signed-off-by: chiyenteng <82238204+chiyenteng@users.noreply.github.com> --- .../AreaNodes_DependentComponentsAdded.py | 2 +- .../TestSuite_Main_Optimized.py | 10 +++++--- .../Entity/EditorEntityContextBus.h | 5 +++- .../Entity/EditorEntityContextComponent.cpp | 23 +++++-------------- .../Entity/EditorEntityContextComponent.h | 5 +--- .../Prefab/PrefabPublicHandler.cpp | 1 + 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/EditorScripts/AreaNodes_DependentComponentsAdded.py b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/EditorScripts/AreaNodes_DependentComponentsAdded.py index c69ce77041..8151299cea 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/EditorScripts/AreaNodes_DependentComponentsAdded.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/EditorScripts/AreaNodes_DependentComponentsAdded.py @@ -69,7 +69,7 @@ def AreaNodes_DependentComponentsAdded(): # Open an existing simple level helper.init_idle() - helper.open_level("Physics", "Base") + helper.open_level("", "Base") # Open Landscape Canvas tool and verify general.open_pane('Landscape Canvas') diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py index 402df133cb..e501b3ad3b 100644 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/landscape_canvas/TestSuite_Main_Optimized.py @@ -12,6 +12,13 @@ import ly_test_tools.environment.file_system as file_system import ly_test_tools._internal.pytest_plugin as internal_plugin from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite +@pytest.mark.SUITE_periodic +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +class TestAutomationWithPrefabSystemEnabled(EditorTestSuite): + + class test_LandscapeCanvas_AreaNodes_DependentComponentsAdded(EditorSharedTest): + from .EditorScripts import AreaNodes_DependentComponentsAdded as test_module @pytest.mark.SUITE_periodic @pytest.mark.parametrize("launcher_platform", ['windows_editor']) @@ -26,9 +33,6 @@ class TestAutomation(EditorTestSuite): class test_LandscapeCanvas_GradientMixer_NodeConstruction(EditorSharedTest): from .EditorScripts import GradientMixer_NodeConstruction as test_module - class test_LandscapeCanvas_AreaNodes_DependentComponentsAdded(EditorSharedTest): - from .EditorScripts import AreaNodes_DependentComponentsAdded as test_module - class test_LandscapeCanvas_AreaNodes_EntityCreatedOnNodeAdd(EditorSharedTest): from .EditorScripts import AreaNodes_EntityCreatedOnNodeAdd as test_module diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextBus.h index 1a5ba60bdf..29d9b742e2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextBus.h @@ -63,9 +63,12 @@ namespace AzToolsFramework /// Registers an existing set of entities with the editor context. virtual void AddEditorEntities(const EntityList& entities) = 0; - /// Registers an existing set of entities with the editor context. + /// Triggers registered callbacks for an existing set of entities with the editor context. virtual void HandleEntitiesAdded(const EntityList& entities) = 0; + /// Creates an editor ready entity, and sends out notification for the creation. + virtual void FinalizeEditorEntity(AZ::Entity* entity) = 0; + /// Destroys an entity in the editor context. /// \return whether or not the entity was destroyed. A false return value signifies the entity did not belong to the game context. virtual bool DestroyEditorEntity(AZ::EntityId entityId) = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.cpp index 470ca8b9ea..95b3f2bf93 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.cpp @@ -227,14 +227,8 @@ namespace AzToolsFramework { AZ::Entity* entity = CreateEntity(name); AZ_Assert(entity != nullptr, "Entity with name %s couldn't be created.", name); - if (m_isLegacySliceService) - { - FinalizeEditorEntity(entity); - } - else - { - EditorEntityContextNotificationBus::Broadcast(&EditorEntityContextNotification::OnEditorEntityCreated, entity->GetId()); - } + FinalizeEditorEntity(entity); + return entity->GetId(); } @@ -263,14 +257,7 @@ namespace AzToolsFramework entity = aznew AZ::Entity(entityId, name); AZ_Assert(entity != nullptr, "Entity with name %s couldn't be created.", name); AddEntity(entity); - if (m_isLegacySliceService) - { - FinalizeEditorEntity(entity); - } - else - { - EditorEntityContextNotificationBus::Broadcast(&EditorEntityContextNotification::OnEditorEntityCreated, entity->GetId()); - } + FinalizeEditorEntity(entity); return entity->GetId(); } @@ -284,10 +271,12 @@ namespace AzToolsFramework { return; } - SetupEditorEntity(entity); // Store creation undo command. + if (m_isLegacySliceService) { + SetupEditorEntity(entity); + ScopedUndoBatch undoBatch("Create Entity"); EntityCreateCommand* command = aznew EntityCreateCommand(static_cast(entity->GetId())); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.h index 28cf67c0f2..4e04b81c67 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/EditorEntityContextComponent.h @@ -81,6 +81,7 @@ namespace AzToolsFramework void AddEditorEntity(AZ::Entity* entity) override; void AddEditorEntities(const EntityList& entities) override; void HandleEntitiesAdded(const EntityList& entities) override; + void FinalizeEditorEntity(AZ::Entity* entity) override; bool CloneEditorEntities(const EntityIdList& sourceEntities, EntityList& resultEntities, AZ::SliceComponent::EntityIdToEntityIdMap& sourceToCloneEntityIdMap) override; @@ -142,13 +143,9 @@ namespace AzToolsFramework } protected: - void OnContextEntitiesAdded(const EntityList& entities) override; void OnContextEntityRemoved(const AZ::EntityId& id) override; - // Helper function for creating editor ready entities. - void FinalizeEditorEntity(AZ::Entity* entity); - void SetupEditorEntity(AZ::Entity* entity); void SetupEditorEntities(const EntityList& entities); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 0c1fb9bed2..62003cd846 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -661,6 +661,7 @@ namespace AzToolsFramework entityOwningInstance.AddEntity(*entity, entityAlias); EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequestBus::Events::HandleEntitiesAdded, EntityList{entity}); + EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequestBus::Events::FinalizeEditorEntity, entity); AZ::Transform transform = AZ::Transform::CreateIdentity(); transform.SetTranslation(position); From 1f7040c6b825361e54e6d502d8452e82543d56b7 Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:28:13 -0600 Subject: [PATCH 28/37] Updating mock calls in unit_test_manifest.py to fix test (#7090) The manifest.py `get_all_templates` method was modified to add a call to `get_all_gems` and `get_gem_templates`. Those calls were not mocked, so they tried to load an o3de_manifest.json file that was on user's machine, which failed on the CI node. Also added a mock call for `load_o3de_manifest`, that validates that it is not called to catch this issue during Automated Review. Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> --- scripts/o3de/tests/unit_test_manifest.py | 39 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/scripts/o3de/tests/unit_test_manifest.py b/scripts/o3de/tests/unit_test_manifest.py index c7d7a7573b..c8eb916bf3 100644 --- a/scripts/o3de/tests/unit_test_manifest.py +++ b/scripts/o3de/tests/unit_test_manifest.py @@ -29,10 +29,17 @@ class TestGetTemplatesForCreation: def get_project_templates() -> list: return [] + @staticmethod + def get_gem_templates() -> list: + return [] + @staticmethod def get_engine_templates() -> list: return [pathlib.Path('D:/o3de/Templates/DefaultProject'), pathlib.Path('D:/o3de/Templates/DefaultGem')] + @staticmethod + def get_all_gems(project_path: pathlib.Path = None) -> list: + return [] @pytest.mark.parametrize("expected_template_paths", [ pytest.param([]) @@ -50,17 +57,25 @@ class TestGetTemplatesForCreation: as get_manifest_templates_patch, \ patch('o3de.manifest.get_project_templates', side_effect=self.get_project_templates)\ as get_project_templates_patch, \ + patch('o3de.manifest.get_gem_templates', side_effect=self.get_gem_templates) \ + as get_gem_templates_patch, \ patch('o3de.manifest.get_engine_templates', side_effect=self.get_engine_templates)\ as get_engine_templates_patch, \ + patch('o3de.manifest.get_all_gems', side_effect=self.get_all_gems) \ + as get_all_gems_patch, \ patch('o3de.validation.valid_o3de_template_json', return_value=True) \ as validate_template_json,\ patch('o3de.validation.valid_o3de_project_json', side_effect=validate_project_json) \ as validate_project_json,\ patch('o3de.validation.valid_o3de_gem_json', side_effect=validate_gem_json) \ - as validate_gem_json: + as validate_gem_json,\ + patch('o3de.manifest.load_o3de_manifest') as load_o3de_manifest_patch: templates = manifest.get_templates_for_generic_creation() assert templates == expected_template_paths + # make sure the o3de manifest isn't attempted to be loaded + load_o3de_manifest_patch.assert_not_called() + @pytest.mark.parametrize("expected_template_paths", [ pytest.param([pathlib.Path('D:/o3de/Templates/DefaultProject')]) @@ -78,17 +93,25 @@ class TestGetTemplatesForCreation: as get_manifest_templates_patch, \ patch('o3de.manifest.get_project_templates', side_effect=self.get_project_templates) \ as get_project_templates_patch, \ + patch('o3de.manifest.get_gem_templates', side_effect=self.get_gem_templates) \ + as get_gem_templates_patch, \ patch('o3de.manifest.get_engine_templates', side_effect=self.get_engine_templates) \ as get_engine_templates_patch, \ + patch('o3de.manifest.get_all_gems', side_effect=self.get_all_gems) \ + as get_all_gems_patch, \ patch('o3de.validation.valid_o3de_template_json', return_value=True) \ as validate_template_json, \ patch('o3de.validation.valid_o3de_project_json', side_effect=validate_project_json) \ as validate_project_json, \ patch('o3de.validation.valid_o3de_gem_json', side_effect=validate_gem_json) \ - as validate_gem_json: + as validate_gem_json,\ + patch('o3de.manifest.load_o3de_manifest') as load_o3de_manifest_patch: templates = manifest.get_templates_for_project_creation() assert templates == expected_template_paths + # make sure the o3de manifest isn't attempted to be loaded + load_o3de_manifest_patch.assert_not_called() + @pytest.mark.parametrize("expected_template_paths", [ pytest.param([pathlib.Path('D:/o3de/Templates/DefaultGem')]) @@ -106,13 +129,21 @@ class TestGetTemplatesForCreation: as get_manifest_templates_patch, \ patch('o3de.manifest.get_project_templates', side_effect=self.get_project_templates) \ as get_project_templates_patch, \ + patch('o3de.manifest.get_gem_templates', side_effect=self.get_gem_templates) \ + as get_gem_templates_patch, \ patch('o3de.manifest.get_engine_templates', side_effect=self.get_engine_templates) \ as get_engine_templates_patch, \ + patch('o3de.manifest.get_all_gems', side_effect=self.get_all_gems) \ + as get_all_gems_patch, \ patch('o3de.validation.valid_o3de_template_json', return_value=True) \ as validate_template_json, \ patch('o3de.validation.valid_o3de_project_json', side_effect=validate_project_json) \ as validate_project_json, \ patch('o3de.validation.valid_o3de_gem_json', side_effect=validate_gem_json) \ - as validate_gem_json: + as validate_gem_json, \ + patch('o3de.manifest.load_o3de_manifest') as load_o3de_manifest_patch: templates = manifest.get_templates_for_gem_creation() - assert templates == expected_template_paths \ No newline at end of file + assert templates == expected_template_paths + + # make sure the o3de manifest isn't attempted to be loaded + load_o3de_manifest_patch.assert_not_called() From 878ba24a3f617c1bed46dad8e0f7ee105f21af26 Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Fri, 21 Jan 2022 18:28:26 -0600 Subject: [PATCH 29/37] Removed unused AWSNativeSDK dependency from Editor (#7091) * Removed unused AWSNativeSDK dependency from Editor Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> * Remove build dependencies on the aws-cpp-sdk library from the Editor Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> --- Code/Editor/CMakeLists.txt | 3 --- Code/Editor/CryEdit.cpp | 10 ---------- Code/Editor/IEditorImpl.cpp | 10 ---------- Code/Editor/IEditorImpl.h | 1 - Code/Editor/MainWindow.cpp | 5 ----- 5 files changed, 29 deletions(-) diff --git a/Code/Editor/CMakeLists.txt b/Code/Editor/CMakeLists.txt index 9d74f8e3a2..3f340e37d8 100644 --- a/Code/Editor/CMakeLists.txt +++ b/Code/Editor/CMakeLists.txt @@ -104,13 +104,11 @@ ly_add_target( 3rdParty::Qt::Concurrent 3rdParty::TIFF 3rdParty::squish-ccr - 3rdParty::AWSNativeSDK::STS Legacy::CryCommon Legacy::EditorCommon AZ::AzCore AZ::AzToolsFramework Gem::LmbrCentral.Static - AZ::AWSNativeSDKInit AZ::AtomCore Gem::Atom_RPI.Edit Gem::Atom_RPI.Public @@ -119,7 +117,6 @@ ly_add_target( Gem::AtomViewportDisplayInfo ${additional_dependencies} PUBLIC - 3rdParty::AWSNativeSDK::Core 3rdParty::Qt::Network Legacy::EditorCore RUNTIME_DEPENDENCIES diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index 8789b31fdc..cb64d47a57 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -36,14 +36,6 @@ AZ_POP_DISABLE_WARNING #include #include -// Aws Native SDK -#include -#include -#include -#include -#include -#include - // AzCore #include #include @@ -144,9 +136,7 @@ AZ_POP_DISABLE_WARNING #include "Plugins/ComponentEntityEditorPlugin/Objects/ComponentEntityObject.h" -// AWSNativeSDK #include -#include #if defined(AZ_PLATFORM_WINDOWS) diff --git a/Code/Editor/IEditorImpl.cpp b/Code/Editor/IEditorImpl.cpp index 38006c6fca..a2712b653d 100644 --- a/Code/Editor/IEditorImpl.cpp +++ b/Code/Editor/IEditorImpl.cpp @@ -16,12 +16,6 @@ // Qt #include -// AWS Native SDK -AZ_PUSH_DISABLE_WARNING(4251 4355 4996, "-Wunknown-warning-option") -#include -#include -AZ_POP_DISABLE_WARNING - // AzCore #include #include @@ -78,9 +72,6 @@ AZ_POP_DISABLE_WARNING // EditorCommon #include -// AWSNativeSDK -#include - #include "Core/QtEditorApplication.h" // for Editor::EditorQtApplication static CCryEditDoc * theDocument; @@ -132,7 +123,6 @@ CEditorImpl::CEditorImpl() , m_pSettingsManager(nullptr) , m_pLevelIndependentFileMan(nullptr) , m_pExportManager(nullptr) - , m_awsResourceManager(nullptr) , m_bMatEditMode(false) , m_bShowStatusText(true) , m_bInitialized(false) diff --git a/Code/Editor/IEditorImpl.h b/Code/Editor/IEditorImpl.h index 7867912941..354f631507 100644 --- a/Code/Editor/IEditorImpl.h +++ b/Code/Editor/IEditorImpl.h @@ -369,7 +369,6 @@ protected: QString m_selectFileBuffer; QString m_levelNameBuffer; - IAWSResourceManager* m_awsResourceManager; std::unique_ptr m_winWidgetManager; //! True if the editor is in material edit mode. Fast preview of materials. diff --git a/Code/Editor/MainWindow.cpp b/Code/Editor/MainWindow.cpp index bbebac96a3..63841299be 100644 --- a/Code/Editor/MainWindow.cpp +++ b/Code/Editor/MainWindow.cpp @@ -11,11 +11,6 @@ #include -// AWs Native SDK -AZ_PUSH_DISABLE_WARNING(4251 4355 4996, "-Wunknown-warning-option") -#include -AZ_POP_DISABLE_WARNING - // Qt #include #include From c4addbc0e0e693f63a8891625ea0d0b9719685fc Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Fri, 21 Jan 2022 22:08:31 -0800 Subject: [PATCH 30/37] Moved AutomatedTesting specific script translation assets from gems/canvas into AutomatedTesting Signed-off-by: Gene Walters --- ...orityToAutonomousNoParamsNotifyEvent.names | 0 .../AuthorityToAutonomousNotifyEvent.names | 0 ...ToAutonomous_PlayerNumberNotifyEvent.names | 56 ++++ ...AuthorityToClientNoParamsNotifyEvent.names | 0 ...tyToClientNoParams_PlayFxNotifyEvent.names | 50 ++++ .../AuthorityToClientNotifyEvent.names | 0 ...nomousToAuthorityNoParamsNotifyEvent.names | 0 .../AutonomousToAuthorityNotifyEvent.names | 0 .../ServerToAuthorityNoParamNotifyEvent.names | 0 .../ServerToAuthorityNotifyEvent.names | 0 ...verToAuthority_DealDamageNotifyEvent.names | 56 ++++ .../Classes/NetworkTestPlayerComponent.names | 242 +++++++++--------- ...tworkTestPlayerComponentNetworkInput.names | 0 13 files changed, 283 insertions(+), 121 deletions(-) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AuthorityToAutonomousNoParamsNotifyEvent.names (100%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AuthorityToAutonomousNotifyEvent.names (100%) create mode 100644 AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomous_PlayerNumberNotifyEvent.names rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AuthorityToClientNoParamsNotifyEvent.names (100%) create mode 100644 AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParams_PlayFxNotifyEvent.names rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AuthorityToClientNotifyEvent.names (100%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AutonomousToAuthorityNoParamsNotifyEvent.names (100%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/AutonomousToAuthorityNotifyEvent.names (100%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/ServerToAuthorityNoParamNotifyEvent.names (100%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/AZEvents/ServerToAuthorityNotifyEvent.names (100%) create mode 100644 AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthority_DealDamageNotifyEvent.names rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/Classes/NetworkTestPlayerComponent.names (90%) rename {Gems/ScriptCanvas/Assets => AutomatedTesting/ScriptCanvas}/TranslationAssets/Classes/NetworkTestPlayerComponentNetworkInput.names (100%) diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToAutonomousNoParamsNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomousNoParamsNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToAutonomousNoParamsNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomousNoParamsNotifyEvent.names diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToAutonomousNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomousNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToAutonomousNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomousNotifyEvent.names diff --git a/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomous_PlayerNumberNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomous_PlayerNumberNotifyEvent.names new file mode 100644 index 0000000000..7255c61d23 --- /dev/null +++ b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToAutonomous_PlayerNumberNotifyEvent.names @@ -0,0 +1,56 @@ +{ + "entries": [ + { + "base": "AuthorityToAutonomous_PlayerNumber Notify Event", + "context": "AZEventHandler", + "variant": "", + "details": { + "name": "Authority To Autonomous_ Player Number Notify Event" + }, + "slots": [ + { + "base": "player_number", + "details": { + "name": "player_number" + } + }, + { + "base": "AuthorityToAutonomous_PlayerNumber Notify Event", + "details": { + "name": "AuthorityToAutonomous_PlayerNumber Notify Event" + } + }, + { + "base": "Connect", + "details": { + "name": "Connect" + } + }, + { + "base": "Disconnect", + "details": { + "name": "Disconnect" + } + }, + { + "base": "On Connected", + "details": { + "name": "On Connected" + } + }, + { + "base": "On Disconnected", + "details": { + "name": "On Disconnected" + } + }, + { + "base": "OnEvent", + "details": { + "name": "OnEvent" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToClientNoParamsNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParamsNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToClientNoParamsNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParamsNotifyEvent.names diff --git a/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParams_PlayFxNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParams_PlayFxNotifyEvent.names new file mode 100644 index 0000000000..3e0207a705 --- /dev/null +++ b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNoParams_PlayFxNotifyEvent.names @@ -0,0 +1,50 @@ +{ + "entries": [ + { + "base": "AuthorityToClientNoParams_PlayFx Notify Event", + "context": "AZEventHandler", + "variant": "", + "details": { + "name": "Authority To Client No Params_ Play Fx Notify Event" + }, + "slots": [ + { + "base": "AuthorityToClientNoParams_PlayFx Notify Event", + "details": { + "name": "AuthorityToClientNoParams_PlayFx Notify Event" + } + }, + { + "base": "Connect", + "details": { + "name": "Connect" + } + }, + { + "base": "Disconnect", + "details": { + "name": "Disconnect" + } + }, + { + "base": "On Connected", + "details": { + "name": "On Connected" + } + }, + { + "base": "On Disconnected", + "details": { + "name": "On Disconnected" + } + }, + { + "base": "OnEvent", + "details": { + "name": "OnEvent" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToClientNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AuthorityToClientNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AuthorityToClientNotifyEvent.names diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AutonomousToAuthorityNoParamsNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AutonomousToAuthorityNoParamsNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AutonomousToAuthorityNoParamsNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AutonomousToAuthorityNoParamsNotifyEvent.names diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AutonomousToAuthorityNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AutonomousToAuthorityNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/AutonomousToAuthorityNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/AutonomousToAuthorityNotifyEvent.names diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/ServerToAuthorityNoParamNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthorityNoParamNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/ServerToAuthorityNoParamNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthorityNoParamNotifyEvent.names diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/ServerToAuthorityNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthorityNotifyEvent.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/AZEvents/ServerToAuthorityNotifyEvent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthorityNotifyEvent.names diff --git a/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthority_DealDamageNotifyEvent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthority_DealDamageNotifyEvent.names new file mode 100644 index 0000000000..2505d9593a --- /dev/null +++ b/AutomatedTesting/ScriptCanvas/TranslationAssets/AZEvents/ServerToAuthority_DealDamageNotifyEvent.names @@ -0,0 +1,56 @@ +{ + "entries": [ + { + "base": "ServerToAuthority_DealDamage Notify Event", + "context": "AZEventHandler", + "variant": "", + "details": { + "name": "Server To Authority_ Deal Damage Notify Event" + }, + "slots": [ + { + "base": "damage", + "details": { + "name": "damage" + } + }, + { + "base": "ServerToAuthority_DealDamage Notify Event", + "details": { + "name": "ServerToAuthority_DealDamage Notify Event" + } + }, + { + "base": "Connect", + "details": { + "name": "Connect" + } + }, + { + "base": "Disconnect", + "details": { + "name": "Disconnect" + } + }, + { + "base": "On Connected", + "details": { + "name": "On Connected" + } + }, + { + "base": "On Disconnected", + "details": { + "name": "On Disconnected" + } + }, + { + "base": "OnEvent", + "details": { + "name": "OnEvent" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/Classes/NetworkTestPlayerComponent.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/Classes/NetworkTestPlayerComponent.names similarity index 90% rename from Gems/ScriptCanvas/Assets/TranslationAssets/Classes/NetworkTestPlayerComponent.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/Classes/NetworkTestPlayerComponent.names index 22636e0453..03e949f784 100644 --- a/Gems/ScriptCanvas/Assets/TranslationAssets/Classes/NetworkTestPlayerComponent.names +++ b/AutomatedTesting/ScriptCanvas/TranslationAssets/Classes/NetworkTestPlayerComponent.names @@ -8,64 +8,6 @@ "name": "Network Test Player Component" }, "methods": [ - { - "base": "AutonomousToAuthority", - "context": "NetworkTestPlayerComponent", - "entry": { - "name": "In", - "tooltip": "When signaled, this will invoke Autonomous To Authority" - }, - "exit": { - "name": "Out", - "tooltip": "Signaled after Autonomous To Authority is invoked" - }, - "details": { - "name": "Autonomous To Authority" - }, - "params": [ - { - "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", - "details": { - "name": "Network Test Player Component" - } - }, - { - "typeid": "{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}", - "details": { - "name": "float" - } - } - ] - }, - { - "base": "ServerToAuthority", - "context": "NetworkTestPlayerComponent", - "entry": { - "name": "In", - "tooltip": "When signaled, this will invoke Server To Authority" - }, - "exit": { - "name": "Out", - "tooltip": "Signaled after Server To Authority is invoked" - }, - "details": { - "name": "Server To Authority" - }, - "params": [ - { - "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", - "details": { - "name": "Network Test Player Component" - } - }, - { - "typeid": "{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}", - "details": { - "name": "float" - } - } - ] - }, { "base": "AutonomousToAuthorityByEntityId", "context": "NetworkTestPlayerComponent", @@ -97,18 +39,72 @@ ] }, { - "base": "ServerToAuthorityByEntityId", + "base": "AuthorityToClientNoParams_PlayFxByEntityId", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Server To Authority By Entity Id" + "tooltip": "When signaled, this will invoke Authority To Client No Params_ Play Fx By Entity Id" }, "exit": { "name": "Out", - "tooltip": "Signaled after Server To Authority By Entity Id is invoked" + "tooltip": "Signaled after Authority To Client No Params_ Play Fx By Entity Id is invoked" }, "details": { - "name": "Server To Authority By Entity Id" + "name": "Authority To Client No Params_ Play Fx By Entity Id" + }, + "params": [ + { + "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", + "details": { + "name": "Source", + "tooltip": "The Source containing the NetworkTestPlayerComponentController" + } + } + ] + }, + { + "base": "AuthorityToAutonomous_PlayerNumberByEntityId", + "context": "NetworkTestPlayerComponent", + "entry": { + "name": "In", + "tooltip": "When signaled, this will invoke Authority To Autonomous_ Player Number By Entity Id" + }, + "exit": { + "name": "Out", + "tooltip": "Signaled after Authority To Autonomous_ Player Number By Entity Id is invoked" + }, + "details": { + "name": "Authority To Autonomous_ Player Number By Entity Id" + }, + "params": [ + { + "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", + "details": { + "name": "Source", + "tooltip": "The Source containing the NetworkTestPlayerComponentController" + } + }, + { + "typeid": "{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}", + "details": { + "name": "player_number" + } + } + ] + }, + { + "base": "ServerToAuthority_DealDamageByEntityId", + "context": "NetworkTestPlayerComponent", + "entry": { + "name": "In", + "tooltip": "When signaled, this will invoke Server To Authority_ Deal Damage By Entity Id" + }, + "exit": { + "name": "Out", + "tooltip": "Signaled after Server To Authority_ Deal Damage By Entity Id is invoked" + }, + "details": { + "name": "Server To Authority_ Deal Damage By Entity Id" }, "params": [ { @@ -121,47 +117,48 @@ { "typeid": "{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}", "details": { - "name": "some Float" + "name": "damage" } } ] }, { - "base": "AutonomousToAuthorityNoParams", + "base": "AutonomousToAuthorityNoParamsByEntityId", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Autonomous To Authority No Params" + "tooltip": "When signaled, this will invoke Autonomous To Authority No Params By Entity Id" }, "exit": { "name": "Out", - "tooltip": "Signaled after Autonomous To Authority No Params is invoked" + "tooltip": "Signaled after Autonomous To Authority No Params By Entity Id is invoked" }, "details": { - "name": "Autonomous To Authority No Params" + "name": "Autonomous To Authority No Params By Entity Id" }, "params": [ { - "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", + "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", "details": { - "name": "Network Test Player Component" + "name": "Source", + "tooltip": "The Source containing the NetworkTestPlayerComponentController" } } ] }, { - "base": "AuthorityToAutonomous", + "base": "ServerToAuthority_DealDamage", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Authority To Autonomous" + "tooltip": "When signaled, this will invoke Server To Authority_ Deal Damage" }, "exit": { "name": "Out", - "tooltip": "Signaled after Authority To Autonomous is invoked" + "tooltip": "Signaled after Server To Authority_ Deal Damage is invoked" }, "details": { - "name": "Authority To Autonomous" + "name": "Server To Authority_ Deal Damage" }, "params": [ { @@ -179,18 +176,18 @@ ] }, { - "base": "AuthorityToClientNoParams", + "base": "AutonomousToAuthority", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Authority To Client No Params" + "tooltip": "When signaled, this will invoke Autonomous To Authority" }, "exit": { "name": "Out", - "tooltip": "Signaled after Authority To Client No Params is invoked" + "tooltip": "Signaled after Autonomous To Authority is invoked" }, "details": { - "name": "Authority To Client No Params" + "name": "Autonomous To Authority" }, "params": [ { @@ -198,6 +195,41 @@ "details": { "name": "Network Test Player Component" } + }, + { + "typeid": "{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}", + "details": { + "name": "float" + } + } + ] + }, + { + "base": "AuthorityToAutonomous_PlayerNumber", + "context": "NetworkTestPlayerComponent", + "entry": { + "name": "In", + "tooltip": "When signaled, this will invoke Authority To Autonomous_ Player Number" + }, + "exit": { + "name": "Out", + "tooltip": "Signaled after Authority To Autonomous_ Player Number is invoked" + }, + "details": { + "name": "Authority To Autonomous_ Player Number" + }, + "params": [ + { + "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", + "details": { + "name": "Network Test Player Component" + } + }, + { + "typeid": "{72039442-EB38-4D42-A1AD-CB68F7E0EEF6}", + "details": { + "name": "int" + } } ] }, @@ -308,79 +340,47 @@ ] }, { - "base": "AuthorityToClientNoParamsByEntityId", + "base": "AutonomousToAuthorityNoParams", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Authority To Client No Params By Entity Id" + "tooltip": "When signaled, this will invoke Autonomous To Authority No Params" }, "exit": { "name": "Out", - "tooltip": "Signaled after Authority To Client No Params By Entity Id is invoked" + "tooltip": "Signaled after Autonomous To Authority No Params is invoked" }, "details": { - "name": "Authority To Client No Params By Entity Id" + "name": "Autonomous To Authority No Params" }, "params": [ { - "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", + "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", "details": { - "name": "Source", - "tooltip": "The Source containing the NetworkTestPlayerComponentController" + "name": "Network Test Player Component" } } ] }, { - "base": "AuthorityToAutonomousByEntityId", + "base": "AuthorityToClientNoParams_PlayFx", "context": "NetworkTestPlayerComponent", "entry": { "name": "In", - "tooltip": "When signaled, this will invoke Authority To Autonomous By Entity Id" + "tooltip": "When signaled, this will invoke Authority To Client No Params_ Play Fx" }, "exit": { "name": "Out", - "tooltip": "Signaled after Authority To Autonomous By Entity Id is invoked" + "tooltip": "Signaled after Authority To Client No Params_ Play Fx is invoked" }, "details": { - "name": "Authority To Autonomous By Entity Id" + "name": "Authority To Client No Params_ Play Fx" }, "params": [ { - "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", + "typeid": "{CA5E5C37-98A6-04D2-E15C-1B4BFEE4C7DD}", "details": { - "name": "Source", - "tooltip": "The Source containing the NetworkTestPlayerComponentController" - } - }, - { - "typeid": "{EA2C3E90-AFBE-44D4-A90D-FAAF79BAF93D}", - "details": { - "name": "some Float" - } - } - ] - }, - { - "base": "AutonomousToAuthorityNoParamsByEntityId", - "context": "NetworkTestPlayerComponent", - "entry": { - "name": "In", - "tooltip": "When signaled, this will invoke Autonomous To Authority No Params By Entity Id" - }, - "exit": { - "name": "Out", - "tooltip": "Signaled after Autonomous To Authority No Params By Entity Id is invoked" - }, - "details": { - "name": "Autonomous To Authority No Params By Entity Id" - }, - "params": [ - { - "typeid": "{6383F1D3-BB27-4E6B-A49A-6409B2059EAA}", - "details": { - "name": "Source", - "tooltip": "The Source containing the NetworkTestPlayerComponentController" + "name": "Network Test Player Component" } } ] diff --git a/Gems/ScriptCanvas/Assets/TranslationAssets/Classes/NetworkTestPlayerComponentNetworkInput.names b/AutomatedTesting/ScriptCanvas/TranslationAssets/Classes/NetworkTestPlayerComponentNetworkInput.names similarity index 100% rename from Gems/ScriptCanvas/Assets/TranslationAssets/Classes/NetworkTestPlayerComponentNetworkInput.names rename to AutomatedTesting/ScriptCanvas/TranslationAssets/Classes/NetworkTestPlayerComponentNetworkInput.names From 1643c68fa7f133ff68eb7405e43e5a22f2901de5 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sun, 23 Jan 2022 18:26:31 -0800 Subject: [PATCH 31/37] chore: fix compiling errors Signed-off-by: Michael Pollind --- .../Input/QtEventToAzInputMapperTests.cpp | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 31193f6f05..78a08af6cb 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -562,7 +562,7 @@ namespace UnitTest AZ::Vector2 accumulatedPosition(0.0f,0.0f); for(const auto& pos: m_azCursorPositions) { - accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(WidgetSize.width(), WidgetSize.height())); + accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2((float)WidgetSize.width(), (float)WidgetSize.height())); } // validate @@ -584,106 +584,106 @@ namespace UnitTest // verify CursorModeWrappedX wrapping MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), QPoint(40, 0), - QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), "CursorModeWrappedX_Test_Right" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, 40, - QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), QPoint(-40, 0), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), "CursorModeWrappedX_Test_Left" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20), QPoint(0, -40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, -20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, -20), "CursorModeWrappedX_Test_Top" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedX, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), QPoint(0, 40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() + 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() + 20), "CursorModeWrappedX_Test_Bottom" }, // verify CursorModeWrappedY wrapping MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), QPoint(40, 0), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() + 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() + 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), "CursorModeWrappedY_Test_Right" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, 40, - QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), QPoint(-40, 0), - QPoint(-20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(-20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), "CursorModeWrappedY_Test_Left" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20), QPoint(0, -40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), "CursorModeWrappedY_Test_Top" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrappedY, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), QPoint(0, 40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20), "CursorModeWrappedY_Test_Bottom" }, // verify CursorModeWrapped wrapping MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), QPoint(40, 0), - QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), "CursorModeWrapped_Test_Right" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, 40, - QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(20, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), QPoint(-40, 0), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() - 20, QtEventToAzInputMapperFixture::WidgetSize.height()/2), "CursorModeWrapped_Test_Left" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20), QPoint(0, -40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), "CursorModeWrapped_Test_Top" }, MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeWrapped, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() - 20), QPoint(0, 40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, 20), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, 20), "CursorModeWrapped_Test_Bottom" }, // verify CursorModeCaptured MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeCaptured, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), QPoint(0, 40), - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), "CursorModeCaptured" }, // verify CursorModeNone MouseMoveParam {AzToolsFramework::CursorInputMode::CursorModeNone, 40, - QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f, QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f), - QPoint(40.0f, 0), - QPoint((QtEventToAzInputMapperFixture::WidgetSize.width() / 2.0f) + 40.0f, (QtEventToAzInputMapperFixture::WidgetSize.height() / 2.0f)), + QPoint(QtEventToAzInputMapperFixture::WidgetSize.width() / 2, QtEventToAzInputMapperFixture::WidgetSize.height() / 2), + QPoint(40, 0), + QPoint((QtEventToAzInputMapperFixture::WidgetSize.width() / 2) + 40, (QtEventToAzInputMapperFixture::WidgetSize.height() / 2)), "CursorModeNone" } ), From 3113b8be03c02b12d9b94b1ae5d4d5eae471a948 Mon Sep 17 00:00:00 2001 From: Steve Pham <82231385+spham-amzn@users.noreply.github.com> Date: Sun, 23 Jan 2022 23:17:25 -0800 Subject: [PATCH 32/37] Fix for 'Cannot Find PThread' when using GCC (#7097) * Fix for 'Cannot Find PThread' when using GCC Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> * Fix error when COMPILATION_C and COMPILATION_CXX handling in ly_append_configurations_options Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> * Add missing 'PARENT_SCOPE' when appending to the CMAKE_C_FLAGS and CMAKE_CXX_FLAGS Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> * Fix appending of CMAKE_C_FLAGS and CMAKE_CXX_FLAGS Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> * Workaround for C/C++ compilation flag settings for gcc. COMPILATION_C/COMPILATION_CXX is wiping out COMPILATION, so define each one separately with no sharing Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> * Remove unnecessary gcc ignore warnings for C files Signed-off-by: Steve Pham <82231385+spham-amzn@users.noreply.github.com> --- cmake/Configurations.cmake | 13 +++++----- .../Common/GCC/Configurations_gcc.cmake | 24 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cmake/Configurations.cmake b/cmake/Configurations.cmake index 63af3d8810..20d94024bb 100644 --- a/cmake/Configurations.cmake +++ b/cmake/Configurations.cmake @@ -42,6 +42,8 @@ include(cmake/ConfigurationTypes.cmake) # \arg:LINK_SHARED # \arg:LINK_SHARED_${CONFIGURATION} # +# Note: COMPILATION_C/COMPILATION_CXX are mutually exclusive with COMPILATION. You can only specify COMPILATION for C/C++ flags or +# a combination of COMPILATION_C/COMPILATION_CXX for the separate c/c++ flags separately. function(ly_append_configurations_options) set(options) @@ -80,15 +82,14 @@ function(ly_append_configurations_options) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILATION_STR}" PARENT_SCOPE) endif() if(ly_append_configurations_options_COMPILATION_C) - string(REPLACE ";" " " COMPILATION_STR "${ly_append_configurations_options_COMPILATION}") - string(APPEND CMAKE_C_FLAGS " " ${COMPILATION_STR}) - set(CMAKE_C_FLAGS ${CMAKE_C_FLAGS} PARENT_SCOPE) + string(REPLACE ";" " " COMPILATION_STR "${ly_append_configurations_options_COMPILATION_C}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMPILATION_STR}" PARENT_SCOPE) endif() if(ly_append_configurations_options_COMPILATION_CXX) - string(REPLACE ";" " " COMPILATION_STR "${ly_append_configurations_options_COMPILATION}") - string(APPEND CMAKE_CXX_FLAGS " " ${COMPILATION_STR}) - set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} PARENT_SCOPE) + string(REPLACE ";" " " COMPILATION_STR "${ly_append_configurations_options_COMPILATION_CXX}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILATION_STR}" PARENT_SCOPE) endif() + if(ly_append_configurations_options_LINK) string(REPLACE ";" " " LINK_STR "${ly_append_configurations_options_LINK}") set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} ${LINK_STR}" PARENT_SCOPE) diff --git a/cmake/Platform/Common/GCC/Configurations_gcc.cmake b/cmake/Platform/Common/GCC/Configurations_gcc.cmake index 17c2474fb8..3db96306ee 100644 --- a/cmake/Platform/Common/GCC/Configurations_gcc.cmake +++ b/cmake/Platform/Common/GCC/Configurations_gcc.cmake @@ -20,7 +20,17 @@ endif() ly_append_configurations_options( - COMPILATION + + COMPILATION_C + -fno-exceptions + -fvisibility=hidden + -Wall + -Werror + + ${LY_GCC_GCOV_FLAGS} + ${LY_GCC_GPROF_FLAGS} + + COMPILATION_CXX -fno-exceptions -fvisibility=hidden -Wall @@ -40,10 +50,8 @@ ly_append_configurations_options( -Wno-unused-value -Wno-unused-variable -Wno-format-truncation - -Wno-reorder -Wno-uninitialized -Wno-array-bounds - -Wno-class-memaccess -Wno-nonnull-compare -Wno-strict-aliasing -Wno-unused-result @@ -58,18 +66,14 @@ ly_append_configurations_options( -Wno-enum-compare -Wno-int-in-bool-context -Wno-sequence-point - -Wno-delete-non-virtual-dtor -Wno-comment - -Wno-reorder -Wno-restrict -Wno-format-overflow - - COMPILATION_C - -Wno-absolute-value - - COMPILATION_CXX -fvisibility-inlines-hidden -Wno-invalid-offsetof + -Wno-class-memaccess + -Wno-delete-non-virtual-dtor + -Wno-reorder COMPILATION_DEBUG -O0 # No optimization From 59409822446d896aed127c587eda277c1d47cde1 Mon Sep 17 00:00:00 2001 From: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> Date: Mon, 24 Jan 2022 10:41:32 +0000 Subject: [PATCH 33/37] Ensure render geometry is refreshed when the mesh component controller is moved (#6452) * ensure render geometry is refreshed when the mesh component controller is moved Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * cache bus pointer and send update notification when mesh changes Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * add integration test to detect MeshComponentController notification to IntersectionNotificationBus Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * remove optimize off Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * update build visibility for AtomLyIntegration editor static lib Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> * move runtime dependencies to gem module Signed-off-by: Tom Hulton-Harrop <82228511+hultonha@users.noreply.github.com> --- .../AzFramework/Render/Intersector.h | 26 ++-- .../CommonFeatures/Code/CMakeLists.txt | 50 +++++++- .../Source/Mesh/MeshComponentController.cpp | 36 ++++-- .../Source/Mesh/MeshComponentController.h | 3 +- .../CommonFeatures/Code/Tests/Main.cpp | 39 ++++++ .../Tests/MeshComponentControllerTests.cpp | 116 ++++++++++++++++++ ...egration_commonfeatures_editor_files.cmake | 1 - ...ion_commonfeatures_editor_test_files.cmake | 11 ++ 8 files changed, 253 insertions(+), 29 deletions(-) create mode 100644 Gems/AtomLyIntegration/CommonFeatures/Code/Tests/Main.cpp create mode 100644 Gems/AtomLyIntegration/CommonFeatures/Code/Tests/MeshComponentControllerTests.cpp create mode 100644 Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_test_files.cmake diff --git a/Code/Framework/AzFramework/AzFramework/Render/Intersector.h b/Code/Framework/AzFramework/AzFramework/Render/Intersector.h index acd6553c32..ba11996a65 100644 --- a/Code/Framework/AzFramework/AzFramework/Render/Intersector.h +++ b/Code/Framework/AzFramework/AzFramework/Render/Intersector.h @@ -5,25 +5,23 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT * */ + #pragma once #include #include #include - #include +#include #include #include -#include - namespace AzFramework { - namespace RenderGeometry { - //! Implementation of IntersectorBus interface, this class contains a cached AABB list - //! of the connected entities to the intersection bus and calculates performant ray intersections against them + //! Implementation of IntersectorBus interface, this class contains a cached AABB list of the connected + //! entities to the intersection bus and calculates efficient ray intersections against them. class Intersector final : public IntersectorBus::Handler , protected IntersectionNotificationBus::Handler @@ -34,32 +32,32 @@ namespace AzFramework Intersector(AzFramework::EntityContextId contextId); ~Intersector(); - // IntersectorBus + // IntersectorBus overrides ... RayResult RayIntersect(const RayRequest& ray) override; - // IntersectionNotifications + // IntersectionNotificationBus overrides ... void OnEntityConnected(AZ::EntityId entityId) override; void OnEntityDisconnected(AZ::EntityId entityId) override; void OnGeometryChanged(AZ::EntityId entityId) override; private: - struct EntityData { EntityData(AZ::EntityId id, AZ::Aabb aabb) : m_id(id) , m_aabb(aabb) - , m_ref(1) {} + , m_ref(1) + { + } AZ::EntityId m_id; AZ::Aabb m_aabb; int m_ref; }; - + class EntityDataList { public: - int AddRef(AZ::EntityId entityId); int RemoveRef(AZ::EntityId entityId); void Update(const EntityData& newData); @@ -88,5 +86,5 @@ namespace AzFramework AZStd::vector> m_candidatesSortedByDist; AzFramework::EntityContextId m_contextId; }; - } -} + } // namespace RenderGeometry +} // namespace AzFramework diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt index 3234db2292..7c31751421 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt @@ -77,8 +77,7 @@ ly_create_alias(NAME AtomLyIntegration_CommonFeatures.Servers NAMESPACE Gem if(PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( - NAME AtomLyIntegration_CommonFeatures.Editor GEM_MODULE - + NAME AtomLyIntegration_CommonFeatures.Editor.Static STATIC NAMESPACE Gem AUTOUIC AUTOMOC @@ -98,7 +97,7 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) PRIVATE ATOMLYINTEGRATION_FEATURE_COMMON_EDITOR BUILD_DEPENDENCIES - PRIVATE + PUBLIC Gem::AtomLyIntegration_CommonFeatures.Static Gem::Atom_RPI.Edit Gem::AtomToolsFramework.Static @@ -108,6 +107,24 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) Legacy::Editor.Headers Legacy::EditorCommon Legacy::CryCommon + ) + + ly_add_target( + NAME AtomLyIntegration_CommonFeatures.Editor GEM_MODULE + NAMESPACE Gem + FILES_CMAKE + atomlyintegration_commonfeatures_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + COMPILE_DEFINITIONS + PRIVATE + ATOMLYINTEGRATION_FEATURE_COMMON_EDITOR + BUILD_DEPENDENCIES + PRIVATE + Gem::AtomLyIntegration_CommonFeatures.Editor.Static RUNTIME_DEPENDENCIES Gem::Atom_RPI.Editor Gem::Atom_Feature_Common.Editor @@ -128,6 +145,33 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) Gem::AtomLyIntegration_CommonFeatures.Editor Gem::GradientSignal.Tools ) + + ################################################################################ + # Tests + ################################################################################ + if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME AtomLyIntegration_CommonFeatures.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + atomlyintegration_commonfeatures_editor_test_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzTestShared + AZ::AzToolsFramework + AZ::AzToolsFrameworkTestCommon + Gem::AtomLyIntegration_CommonFeatures.Static + Gem::AtomLyIntegration_CommonFeatures.Editor.Static + ) + ly_add_googletest( + NAME Gem::AtomLyIntegration_CommonFeatures.Editor.Tests + ) + endif() endif() # AtomLyIntegration_CommonFeatures gem targets are required as part of the Editor and AssetProcessor diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp index a68ad24adf..e26d328e17 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.cpp @@ -82,7 +82,6 @@ namespace AZ ->Field("MinimumScreenCoverage", &MeshComponentConfig::m_minimumScreenCoverage) ->Field("QualityDecayRate", &MeshComponentConfig::m_qualityDecayRate); } - } bool MeshComponentConfig::IsAssetSet() @@ -225,33 +224,42 @@ namespace AZ { } + static AzFramework::EntityContextId FindOwningContextId(const AZ::EntityId entityId) + { + AzFramework::EntityContextId contextId = AzFramework::EntityContextId::CreateNull(); + AzFramework::EntityIdContextQueryBus::EventResult( + contextId, entityId, &AzFramework::EntityIdContextQueries::GetOwningContextId); + return contextId; + } + void MeshComponentController::Activate(const AZ::EntityComponentIdPair& entityComponentIdPair) { const AZ::EntityId entityId = entityComponentIdPair.GetEntityId(); m_entityComponentIdPair = entityComponentIdPair; m_transformInterface = TransformBus::FindFirstHandler(entityId); - AZ_Warning("MeshComponentController", m_transformInterface, "Unable to attach to a TransformBus handler. This mesh will always be rendered at the origin."); + AZ_Warning( + "MeshComponentController", m_transformInterface, + "Unable to attach to a TransformBus handler. This mesh will always be rendered at the origin."); m_meshFeatureProcessor = RPI::Scene::GetFeatureProcessorForEntity(entityId); AZ_Error("MeshComponentController", m_meshFeatureProcessor, "Unable to find a MeshFeatureProcessorInterface on the entityId."); m_cachedNonUniformScale = AZ::Vector3::CreateOne(); AZ::NonUniformScaleRequestBus::EventResult(m_cachedNonUniformScale, entityId, &AZ::NonUniformScaleRequests::GetScale); - AZ::NonUniformScaleRequestBus::Event(entityId, &AZ::NonUniformScaleRequests::RegisterScaleChangedEvent, - m_nonUniformScaleChangedHandler); + AZ::NonUniformScaleRequestBus::Event( + entityId, &AZ::NonUniformScaleRequests::RegisterScaleChangedEvent, m_nonUniformScaleChangedHandler); + const auto entityContextId = FindOwningContextId(entityId); MeshComponentRequestBus::Handler::BusConnect(entityId); TransformNotificationBus::Handler::BusConnect(entityId); MaterialReceiverRequestBus::Handler::BusConnect(entityId); MaterialComponentNotificationBus::Handler::BusConnect(entityId); AzFramework::BoundsRequestBus::Handler::BusConnect(entityId); - AzFramework::EntityContextId contextId; - AzFramework::EntityIdContextQueryBus::EventResult( - contextId, entityId, &AzFramework::EntityIdContextQueries::GetOwningContextId); - AzFramework::RenderGeometry::IntersectionRequestBus::Handler::BusConnect({entityId, contextId}); + AzFramework::RenderGeometry::IntersectionRequestBus::Handler::BusConnect({ entityId, entityContextId }); + AzFramework::RenderGeometry::IntersectionNotificationBus::Bind(m_intersectionNotificationBus, entityContextId); - //Buses must be connected before RegisterModel in case requests are made as a result of HandleModelChange + // Buses must be connected before RegisterModel in case requests are made as a result of HandleModelChange RegisterModel(); } @@ -291,6 +299,11 @@ namespace AZ { m_meshFeatureProcessor->SetTransform(m_meshHandle, world, m_cachedNonUniformScale); } + + // ensure the render geometry is kept in sync with any changes to the entity the mesh is on + AzFramework::RenderGeometry::IntersectionNotificationBus::Event( + m_intersectionNotificationBus, &AzFramework::RenderGeometry::IntersectionNotificationBus::Events::OnGeometryChanged, + m_entityComponentIdPair.GetEntityId()); } void MeshComponentController::HandleNonUniformScaleChange(const AZ::Vector3& nonUniformScale) @@ -301,7 +314,7 @@ namespace AZ m_meshFeatureProcessor->SetTransform(m_meshHandle, m_transformInterface->GetWorldTM(), m_cachedNonUniformScale); } } - + RPI::ModelMaterialSlotMap MeshComponentController::GetModelMaterialSlots() const { Data::Asset modelAsset = GetModelAsset(); @@ -369,6 +382,9 @@ namespace AZ MeshComponentNotificationBus::Event(entityId, &MeshComponentNotificationBus::Events::OnModelReady, m_configuration.m_modelAsset, model); MaterialReceiverNotificationBus::Event(entityId, &MaterialReceiverNotificationBus::Events::OnMaterialAssignmentsChanged); AZ::Interface::Get()->RefreshEntityLocalBoundsUnion(entityId); + AzFramework::RenderGeometry::IntersectionNotificationBus::Event( + m_intersectionNotificationBus, &AzFramework::RenderGeometry::IntersectionNotificationBus::Events::OnGeometryChanged, + m_entityComponentIdPair.GetEntityId()); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h index 76cf6a1b39..cecd744b80 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Mesh/MeshComponentController.h @@ -162,6 +162,8 @@ namespace AZ bool m_isVisible = true; MeshComponentConfig m_configuration; AZ::Vector3 m_cachedNonUniformScale = AZ::Vector3::CreateOne(); + //! Cached bus to use to notify RenderGeometry::Intersector the entity/component has changed. + AzFramework::RenderGeometry::IntersectionNotificationBus::BusPtr m_intersectionNotificationBus; MeshFeatureProcessorInterface::ModelChangedEvent::Handler m_changeEventHandler { @@ -173,6 +175,5 @@ namespace AZ [&](const AZ::Vector3& nonUniformScale) { HandleNonUniformScaleChange(nonUniformScale); } }; }; - } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/Main.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/Main.cpp new file mode 100644 index 0000000000..2d75cf96f5 --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/Main.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include + +class AtomLyIntegrationHook : public AZ::Test::ITestEnvironment +{ +public: + void SetupEnvironment() override + { + AZ::AllocatorInstance::Create(); + } + + void TeardownEnvironment() override + { + AZ::AllocatorInstance::Destroy(); + } +}; + +// required to support running integration tests with Qt +AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv) +{ + ::testing::InitGoogleMock(&argc, argv); + QApplication app(argc, argv); + AZ::Test::printUnusedParametersWarning(argc, argv); + AZ::Test::addTestEnvironments({ DEFAULT_UNIT_TEST_ENV, new AtomLyIntegrationHook }); + int result = RUN_ALL_TESTS(); + return result; +} + +IMPLEMENT_TEST_EXECUTABLE_MAIN(); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/MeshComponentControllerTests.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/MeshComponentControllerTests.cpp new file mode 100644 index 0000000000..bc1bf3a247 --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Tests/MeshComponentControllerTests.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +#include +#include + +namespace UnitTest +{ + static AzFramework::EntityContextId FindOwningContextId(const AZ::EntityId entityId) + { + AzFramework::EntityContextId contextId = AzFramework::EntityContextId::CreateNull(); + AzFramework::EntityIdContextQueryBus::EventResult(contextId, entityId, &AzFramework::EntityIdContextQueries::GetOwningContextId); + return contextId; + } + + class IntersectionNotificationDetector : public AzFramework::RenderGeometry::IntersectionNotificationBus::Handler + { + public: + void Connect(const AzFramework::EntityContextId& entityContextId); + void Disconnect(); + + // IntersectionNotificationBus overrides ... + void OnEntityConnected(AZ::EntityId entityId) override; + void OnEntityDisconnected(AZ::EntityId entityId) override; + void OnGeometryChanged(AZ::EntityId entityId) override; + + AZ::EntityId m_lastEntityIdChanged; + }; + + void IntersectionNotificationDetector::Connect(const AzFramework::EntityContextId& entityContextId) + { + AzFramework::RenderGeometry::IntersectionNotificationBus::Handler::BusConnect(entityContextId); + } + + void IntersectionNotificationDetector::Disconnect() + { + AzFramework::RenderGeometry::IntersectionNotificationBus::Handler::BusDisconnect(); + } + + void IntersectionNotificationDetector::OnEntityConnected([[maybe_unused]] AZ::EntityId entityId) + { + } + + void IntersectionNotificationDetector::OnEntityDisconnected([[maybe_unused]] AZ::EntityId entityId) + { + } + + void IntersectionNotificationDetector::OnGeometryChanged(AZ::EntityId entityId) + { + m_lastEntityIdChanged = entityId; + } + + class MeshComponentControllerFixture : public ToolsApplicationFixture + { + public: + void SetUpEditorFixtureImpl() override + { + m_meshComponentDescriptor = AZStd::unique_ptr(AZ::Render::MeshComponent::CreateDescriptor()); + m_meshComponentDescriptor->Reflect(GetApplication()->GetSerializeContext()); + + m_editorMeshComponentDescriptor = + AZStd::unique_ptr(AZ::Render::EditorMeshComponent::CreateDescriptor()); + m_editorMeshComponentDescriptor->Reflect(GetApplication()->GetSerializeContext()); + + m_entityId1 = CreateDefaultEditorEntity("Entity1"); + m_entityIds.push_back(m_entityId1); + + m_intersectionNotificationDetector.Connect(FindOwningContextId(m_entityId1)); + } + + void TearDownEditorFixtureImpl() override + { + bool entityDestroyed = false; + AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult( + entityDestroyed, &AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, m_entityId1); + + m_intersectionNotificationDetector.Disconnect(); + + m_meshComponentDescriptor.reset(); + m_editorMeshComponentDescriptor.reset(); + } + + AZ::EntityId m_entityId1; + AzToolsFramework::EntityIdList m_entityIds; + AZStd::unique_ptr m_meshComponentDescriptor; + AZStd::unique_ptr m_editorMeshComponentDescriptor; + IntersectionNotificationDetector m_intersectionNotificationDetector; + }; + + TEST_F(MeshComponentControllerFixture, IntersectionNotificationBusIsNotifiedWhenMeshComponentControllerTransformIsModified) + { + auto* entity1 = AzToolsFramework::GetEntityById(m_entityId1); + entity1->Deactivate(); + entity1->CreateComponent(); + // note: RPI::Scene::GetFeatureProcessorForEntity(...) returns nullptr + // and so m_meshFeatureProcessor is null + AZ_TEST_START_TRACE_SUPPRESSION; + entity1->Activate(); + AZ_TEST_STOP_TRACE_SUPPRESSION(1); + + AZ::TransformBus::Event( + m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f))); + + using ::testing::Eq; + EXPECT_THAT(m_entityId1, Eq(m_intersectionNotificationDetector.m_lastEntityIdChanged)); + } +} // namespace UnitTest diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_files.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_files.cmake index 0dea725d70..018795dcf2 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_files.cmake +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_files.cmake @@ -10,7 +10,6 @@ set(FILES Include/AtomLyIntegration/CommonFeatures/Material/EditorMaterialSystemComponentNotificationBus.h Include/AtomLyIntegration/CommonFeatures/Material/EditorMaterialSystemComponentRequestBus.h Include/AtomLyIntegration/CommonFeatures/ReflectionProbe/EditorReflectionProbeBus.h - Source/Module.cpp Source/Animation/EditorAttachmentComponent.h Source/Animation/EditorAttachmentComponent.cpp Source/EditorCommonFeaturesSystemComponent.h diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_test_files.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_test_files.cmake new file mode 100644 index 0000000000..5a58124e0a --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/atomlyintegration_commonfeatures_editor_test_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Tests/Main.cpp + Tests/MeshComponentControllerTests.cpp) From d412f3cc502d598464422444ea991cd0cfb0ab84 Mon Sep 17 00:00:00 2001 From: greerdv Date: Mon, 24 Jan 2022 12:38:15 +0000 Subject: [PATCH 34/37] update with feedback from PR Signed-off-by: greerdv --- .../Source/EditorShapeColliderComponent.cpp | 17 ++++++++--------- .../Code/Source/EditorShapeColliderComponent.h | 4 ++-- .../Code/Tests/ShapeColliderComponentTests.cpp | 16 +++++++++------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp index bdf70fc227..d99a76f306 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp @@ -55,7 +55,7 @@ namespace PhysX m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::Offset, false); } - AZ::Crc32 EditorShapeColliderComponent::SubdivisionCountVisibility() + AZ::Crc32 EditorShapeColliderComponent::SubdivisionCountVisibility() const { if (m_shapeType == ShapeType::Cylinder) { @@ -65,7 +65,7 @@ namespace PhysX return AZ::Edit::PropertyVisibility::Hide; } - AZ::Crc32 EditorShapeColliderComponent::SingleSidedVisibility() + AZ::Crc32 EditorShapeColliderComponent::SingleSidedVisibility() const { if ((m_shapeType == ShapeType::QuadSingleSided || m_shapeType == ShapeType::QuadDoubleSided) && GetEntity()->FindComponent() == nullptr) @@ -124,16 +124,16 @@ namespace PhysX void EditorShapeColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { - provided.push_back(AZ_CRC("PhysicsWorldBodyService", 0x944da0cc)); - provided.push_back(AZ_CRC("PhysXColliderService", 0x4ff43f7c)); - provided.push_back(AZ_CRC("PhysXTriggerService", 0x3a117d7b)); - provided.push_back(AZ_CRC("PhysXShapeColliderService", 0x98a7e779)); + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXTriggerService")); + provided.push_back(AZ_CRC_CE("PhysXShapeColliderService")); } void EditorShapeColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) { - required.push_back(AZ_CRC("TransformService", 0x8ee22c50)); - required.push_back(AZ_CRC("ShapeService", 0xe86aa5fe)); + required.push_back(AZ_CRC_CE("TransformService")); + required.push_back(AZ_CRC_CE("ShapeService")); } void EditorShapeColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) @@ -422,7 +422,6 @@ namespace PhysX SetShapeConfig(ShapeType::QuadSingleSided, shapeConfig); } - else { // it's not possible to create a perfectly 2d convex in PhysX, so the best we can do is a very thin box diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h index 0f5350b781..09d5f803fc 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h @@ -105,9 +105,9 @@ namespace PhysX void RefreshUiProperties(); AZ::u32 OnSubdivisionCountChange(); - AZ::Crc32 SubdivisionCountVisibility(); + AZ::Crc32 SubdivisionCountVisibility() const; void OnSingleSidedChange(); - AZ::Crc32 SingleSidedVisibility(); + AZ::Crc32 SingleSidedVisibility() const; // AZ::Component void Activate() override; diff --git a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp index c164e5de63..c9dcbe64d1 100644 --- a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp @@ -470,7 +470,7 @@ namespace PhysXEditorTests void SetTrigger(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent, bool isTrigger) { - SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC("Trigger", 0x1a6b0f5d), isTrigger); + SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC_CE("Trigger"), isTrigger); } bool GetBoolValueFromComponent(AZ::Component* component, AZ::Crc32 name) @@ -489,17 +489,17 @@ namespace PhysXEditorTests bool IsTrigger(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent) { - return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC("Trigger")); + return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC_CE("Trigger")); } void SetSingleSided(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent, bool singleSided) { - SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC("SingleSided"), singleSided); + SetBoolValueOnComponent(editorShapeColliderComponent, AZ_CRC_CE("SingleSided"), singleSided); } bool IsSingleSided(PhysX::EditorShapeColliderComponent* editorShapeColliderComponent) { - return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC("SingleSided")); + return GetBoolValueFromComponent(editorShapeColliderComponent, AZ_CRC_CE("SingleSided")); } EntityPtr CreateRigidBox(const AZ::Vector3& boxDimensions, const AZ::Vector3& position) @@ -636,7 +636,6 @@ namespace PhysXEditorTests EXPECT_NEAR(aabb.GetMin().GetY(), -0.6f, 1e-3f); EXPECT_NEAR(aabb.GetMax().GetX(), 2.7f, 1e-3f); EXPECT_NEAR(aabb.GetMax().GetY(), 0.6f, 1e-3f); - EXPECT_TRUE(true); } TEST_P(PhysXEditorParamBoolFixture, EditorShapeColliderComponent_TriggerSettingIsRememberedWhenSwitchingToQuadAndBack) @@ -731,9 +730,12 @@ namespace PhysXEditorTests EntityPtr gameQuadEntity = CreateActiveGameEntityFromEditorEntity(editorQuadEntity.get()); EntityPtr gameBoxEntity = CreateActiveGameEntityFromEditorEntity(editorBoxEntity.get()); - // give the box enough upward velocity to rise above the level of the quad and simulate + // give the box enough upward velocity to rise above the level of the quad + // simulate for enough time that the box would have reached the top of its trajectory and fallen back past the starting point if + // it hadn't collided with the top of the quad + const int numTimesteps = 100; Physics::RigidBodyRequestBus::Event(gameBoxEntity->GetId(), &Physics::RigidBodyRequests::SetLinearVelocity, AZ::Vector3::CreateAxisZ(6.0f)); - PhysX::TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, 200); + PhysX::TestUtils::UpdateScene(m_defaultScene, AzPhysics::SystemConfiguration::DefaultFixedTimestep, numTimesteps); // the box should travel through the base of the quad because it has no collision from that direction // and land on the top surface of the quad, which does have collision From 7b649656bc870de433e0d6a8db6eb63b230a6cc5 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Mon, 24 Jan 2022 07:30:39 -0800 Subject: [PATCH 35/37] chore: use numeric cast for QtEventTOAzInputMapperTest Signed-off-by: Michael Pollind --- .../Tests/Input/QtEventToAzInputMapperTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp index 78a08af6cb..8222a115f6 100644 --- a/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Input/QtEventToAzInputMapperTests.cpp @@ -562,7 +562,7 @@ namespace UnitTest AZ::Vector2 accumulatedPosition(0.0f,0.0f); for(const auto& pos: m_azCursorPositions) { - accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2((float)WidgetSize.width(), (float)WidgetSize.height())); + accumulatedPosition += (pos.m_normalizedPositionDelta * AZ::Vector2(aznumeric_cast(WidgetSize.width()), aznumeric_cast(WidgetSize.height()))); } // validate From 37c0b661f94ffbde91364ff8536dc916dcc48c6c Mon Sep 17 00:00:00 2001 From: moraaar Date: Mon, 24 Jan 2022 15:49:33 +0000 Subject: [PATCH 36/37] Added blast asset, whose fbx works with Atom, to help testing Blast Gem (#7110) Signed-off-by: moraaar --- .../Destruction/cinder_wall_complex.blast | Bin 0 -> 330848 bytes .../Assets/Destruction/cinder_wall_complex.fbx | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 AutomatedTesting/Assets/Destruction/cinder_wall_complex.blast create mode 100644 AutomatedTesting/Assets/Destruction/cinder_wall_complex.fbx diff --git a/AutomatedTesting/Assets/Destruction/cinder_wall_complex.blast b/AutomatedTesting/Assets/Destruction/cinder_wall_complex.blast new file mode 100644 index 0000000000000000000000000000000000000000..fcf67594c77c4fb51cc722d3d982a5b806a55fb4 GIT binary patch literal 330848 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⁡oo-r}+m3wDXFz&_Uv{b#sOK&*O#B9dC$e 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 literal 0 HcmV?d00001 diff --git a/AutomatedTesting/Assets/Destruction/cinder_wall_complex.fbx b/AutomatedTesting/Assets/Destruction/cinder_wall_complex.fbx new file mode 100644 index 0000000000..62c305c35e --- /dev/null +++ b/AutomatedTesting/Assets/Destruction/cinder_wall_complex.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f94f2634eacb4d7bee20dacc45edef96e4d268f1adb7960b8aa8f3b6e2906ed +size 6867609 From c7d1c4603f80174b57ae217dbb801a3875d77499 Mon Sep 17 00:00:00 2001 From: mrieggeramzn <61609885+mrieggeramzn@users.noreply.github.com> Date: Mon, 24 Jan 2022 09:24:20 -0800 Subject: [PATCH 37/37] Removing lux core (whats left of it) (#7089) * Removing lux core (whats left of it) Signed-off-by: mrieggeramzn * Removing files recommended by NLawson and EPapp Signed-off-by: mrieggeramzn --- Gems/Atom/Feature/Common/Code/CMakeLists.txt | 1 - .../Common/Clang/atom_feature_common_clang.cmake | 7 ------- .../Common/GCC/atom_feature_common_gcc.cmake | 13 ------------- .../Common/MSVC/atom_feature_common_msvc.cmake | 7 ------- 4 files changed, 28 deletions(-) delete mode 100644 Gems/Atom/Feature/Common/Code/Source/Platform/Common/Clang/atom_feature_common_clang.cmake delete mode 100644 Gems/Atom/Feature/Common/Code/Source/Platform/Common/GCC/atom_feature_common_gcc.cmake delete mode 100644 Gems/Atom/Feature/Common/Code/Source/Platform/Common/MSVC/atom_feature_common_msvc.cmake diff --git a/Gems/Atom/Feature/Common/Code/CMakeLists.txt b/Gems/Atom/Feature/Common/Code/CMakeLists.txt index a832c46539..0d779cc3ea 100644 --- a/Gems/Atom/Feature/Common/Code/CMakeLists.txt +++ b/Gems/Atom/Feature/Common/Code/CMakeLists.txt @@ -18,7 +18,6 @@ ly_add_target( ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake PLATFORM_INCLUDE_FILES ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake - ${common_source_dir}/${PAL_TRAIT_COMPILER_ID}/atom_feature_common_${PAL_TRAIT_COMPILER_ID_LOWERCASE}.cmake INCLUDE_DIRECTORIES PRIVATE . diff --git a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/Clang/atom_feature_common_clang.cmake b/Gems/Atom/Feature/Common/Code/Source/Platform/Common/Clang/atom_feature_common_clang.cmake deleted file mode 100644 index 7a325ca97e..0000000000 --- a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/Clang/atom_feature_common_clang.cmake +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (c) Contributors to the Open 3D Engine Project. -# For complete copyright and license terms please see the LICENSE at the root of this distribution. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT -# -# diff --git a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/GCC/atom_feature_common_gcc.cmake b/Gems/Atom/Feature/Common/Code/Source/Platform/Common/GCC/atom_feature_common_gcc.cmake deleted file mode 100644 index b3f7867308..0000000000 --- a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/GCC/atom_feature_common_gcc.cmake +++ /dev/null @@ -1,13 +0,0 @@ -# -# Copyright (c) Contributors to the Open 3D Engine Project. -# For complete copyright and license terms please see the LICENSE at the root of this distribution. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT -# -# - -set_source_files_properties( - Source/LuxCore/LuxCoreRenderer.cpp - PROPERTIES - COMPILE_OPTIONS -fexceptions -) diff --git a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/MSVC/atom_feature_common_msvc.cmake b/Gems/Atom/Feature/Common/Code/Source/Platform/Common/MSVC/atom_feature_common_msvc.cmake deleted file mode 100644 index 7a325ca97e..0000000000 --- a/Gems/Atom/Feature/Common/Code/Source/Platform/Common/MSVC/atom_feature_common_msvc.cmake +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (c) Contributors to the Open 3D Engine Project. -# For complete copyright and license terms please see the LICENSE at the root of this distribution. -# -# SPDX-License-Identifier: Apache-2.0 OR MIT -# -#