Merge branch 'main' into jckand/FoundationAuto

main
jckand 5 years ago
commit dd00b8a1ad

4
.gitignore vendored

@ -6,8 +6,8 @@ AssetProcessorTemp/**
[Cc]ache/
Editor/EditorEventLog.xml
Editor/EditorLayout.xml
Tools/**/*egg-info/**
Tools/**/*egg-link
**/*egg-info/**
**/*egg-link
UserSettings.xml
[Uu]ser/
FrameCapture/**

@ -33,9 +33,6 @@ Assuming CMake is already setup on your operating system, below are some sample
mkdir windows_vs2019
cd windows_vs2019
cmake .. -G "Visual Studio 16 2019" -A x64 -T host=x64 -DLY_3RDPARTY_PATH="%3RDPARTYPATH%" -DLY_PROJECTS=AutomatedTesting
NOTE:
Using the above command also adds EditorPythonTestTools to the PYTHONPATH OS environment variable.
Additionally, some CTest scripts will add the Python interpreter path to the PYTHON OS environment variable.
To manually install the project in development mode using your own installed Python interpreter:
cd /path/to/od3e/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools

@ -1,110 +0,0 @@
Metadata-Version: 1.0
Name: editor-python-test-tools
Version: 1.0.0
Summary: Lumberyard editor Python bindings test tools
Home-page: UNKNOWN
Author: UNKNOWN
Author-email: UNKNOWN
License: UNKNOWN
Description: All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
INTRODUCTION
------------
EditorPythonBindings is a Python project that contains a collection of testing tools
developed by the Lumberyard Test Tech team. The project contains
the following tools:
* Workspace Manager:
A library to manipulate Lumberyard installations
* Launchers:
A library to test the game in a variety of platforms
REQUIREMENTS
------------
* Python 3.7.5 (64-bit)
It is recommended that you completely remove any other versions of Python
installed on your system.
INSTALL
-----------
It is recommended to set up these these tools with Lumberyard's CMake build commands.
Assuming CMake is already setup on your operating system, below are some sample build commands:
cd /path/to/od3e/
mkdir windows_vs2019
cd windows_vs2019
cmake .. -G "Visual Studio 16 2019" -A x64 -T host=x64 -DLY_3RDPARTY_PATH="%3RDPARTYPATH%" -DLY_PROJECTS=AutomatedTesting
NOTE:
Using the above command also adds LyTestTools to the PYTHONPATH OS environment variable.
Additionally, some CTest scripts will add the Python interpreter path to the PYTHON OS environment variable.
There is some LyTestTools functionality that will search for these, so feel free to populate them manually.
To manually install the project in development mode using your own installed Python interpreter:
cd /path/to/lumberyard/dev/Tools/LyTestTools/
/path/to/your/python -m pip install -e .
For console/mobile testing, update the following .ini file in your root user directory:
i.e. C:/Users/myusername/ly_test_tools/devices.ini (a.k.a. %USERPROFILE%/ly_test_tools/devices.ini)
You will need to add a section for the device, and a key holding the device identifier value (usually an IP or ID).
It should look similar to this for each device:
[android]
id = 988939353955305449
[gameconsole]
ip = 192.168.1.1
[gameconsole2]
ip = 192.168.1.2
PACKAGE STRUCTURE
-----------------
The project is organized into packages. Each package corresponds to a tool:
- LyTestTools.ly_test_tools._internal: contains logging setup, pytest fixture, and o3de workspace manager modules
- LyTestTools.ly_test_tools.builtin: builtin helpers and fixtures for quickly writing tests
- LyTestTools.ly_test_tools.console: modules used for consoles
- LyTestTools.ly_test_tools.environment: functions related to file/process management and cleanup
- LyTestTools.ly_test_tools.image: modules related to image capturing and processing
- LyTestTools.ly_test_tools.launchers: game launchers library
- LyTestTools.ly_test_tools.log: modules for interacting with generated or existing log files
- LyTestTools.ly_test_tools.o3de: modules used to interact with Open 3D Engine
- LyTestTools.ly_test_tools.mobile: modules used for android/ios
- LyTestTools.ly_test_tools.report: modules used for reporting
- LyTestTools.tests: LyTestTools integration, unit, and example usage tests
DIRECTORY STRUCTURE
-------------------
The directory structure corresponds to the package structure. For example, the
ly_test_tools.builtin package is located in the ly_test_tools/builtin/ directory.
ENTRY POINTS
------------
Deploying the project in development mode installs only entry points for pytest fixtures.
UNINSTALLATION
--------------
The preferred way to uninstall the project is:
/path/to/your/python -m pip uninstall ly_test_tools
Platform: UNKNOWN

@ -1,7 +0,0 @@
README.txt
setup.py
editor_python_test_tools.egg-info/PKG-INFO
editor_python_test_tools.egg-info/SOURCES.txt
editor_python_test_tools.egg-info/dependency_links.txt
editor_python_test_tools.egg-info/requires.txt
editor_python_test_tools.egg-info/top_level.txt

File diff suppressed because it is too large Load Diff

@ -21,6 +21,7 @@ if(CMAKE_VERSION VERSION_EQUAL 3.19)
cmake_policy(SET CMP0111 OLD)
endif()
include(cmake/LySet.cmake)
include(cmake/Version.cmake)
include(cmake/OutputDirectory.cmake)
@ -74,9 +75,6 @@ foreach(restricted_platform ${PAL_RESTRICTED_PLATFORMS})
endif()
endforeach()
# Recurse into directory of general python test scripts
add_subdirectory(ctest_scripts)
add_subdirectory(scripts)
# SPEC-1417 will investigate and fix this
@ -125,4 +123,7 @@ ly_test_impact_post_step()
# 6. Generate the O3DE find file and setup install locations for scripts, tools, assets etc., required by the engine
if(NOT INSTALLED_ENGINE)
ly_setup_o3de_install()
# IMPORTANT: must be included last
include(cmake/CPack.cmake)
endif()

@ -185,6 +185,7 @@ string CCmdLine::Next(char*& src)
return string(org, src - 1);
case ' ':
ch = *src++;
continue;
default:
org = src - 1;

@ -1104,8 +1104,11 @@ namespace AZ
// If we either already had valid asset data, or just created it via FindOrCreateAsset, try to queue the load.
if (m_assetData && m_assetData->GetId().IsValid())
{
// Only try to queue if the asset isn't already loading or loaded.
if (m_assetData->GetStatus() == AZ::Data::AssetData::AssetStatus::NotLoaded)
// Try to queue if the asset isn't already loading or loaded.
// Also try to queue if the asset *is* already loading or loaded, but we're the only one with a strong reference
// (i.e. use count == 1), because that means it was in the process of being garbage-collected.
if ((m_assetData->GetStatus() == AZ::Data::AssetData::AssetStatus::NotLoaded) ||
(m_assetData->GetUseCount() == 1))
{
*this = AssetInternal::GetAsset(m_assetData->GetId(), m_assetData->GetType(), loadBehavior, loadParams);
}

@ -255,7 +255,7 @@ namespace AZ
bool AssetContainer::IsValid() const
{
return (m_containerAssetId.IsValid() && m_initComplete);
return (m_containerAssetId.IsValid() && m_initComplete && m_rootAsset);
}
void AssetContainer::CheckReady()
@ -341,6 +341,7 @@ namespace AZ
void AssetContainer::OnAssetError(Asset<AssetData> asset)
{
AZ_Warning("AssetContainer", false, "Error loading asset %s", asset->GetId().ToString<AZStd::string>().c_str());
HandleReadyAsset(asset);
}
@ -366,7 +367,10 @@ namespace AZ
auto remainingPreloadIter = m_preloadList.find(waiterId);
if (remainingPreloadIter == m_preloadList.end())
{
AZ_Warning("AssetContainer", !m_initComplete, "Couldn't find waiting list for %s", waiterId.ToString<AZStd::string>().c_str());
// If we got here without an entry on the preload list, it probably means this asset was triggered to load multiple
// times, some with dependencies and some without. To ensure that we don't disturb the loads that expect the
// dependencies, just silently return and don't treat the asset as finished loading. We'll rely on the other load
// to send an OnAssetReady() whenever its expected dependencies are met.
return;
}
if (!remainingPreloadIter->second.erase(preloadID))
@ -609,11 +613,16 @@ namespace AZ
++thisListPair;
}
for(auto& thisList : preloadList)
{
// Only save the entry to the final preload list if it has at least one dependent asset still remaining after
// the checks above.
if (!thisList.second.empty())
{
m_preloadList[thisList.first].insert(thisList.second.begin(), thisList.second.end());
}
}
}
}
bool AssetContainer::HasPreloads(const AssetId& assetId) const
{

@ -208,6 +208,7 @@ 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.");
// 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())
@ -222,6 +223,16 @@ namespace AZ::Data
AZ_PROFILE_INTERVAL_END(AZ::Debug::ProfileCategory::AzCore, this);
}
void AssetDataStream::RequestCancel()
{
AZStd::scoped_lock<AZStd::mutex> lock(m_readRequestMutex);
if (m_curReadRequest)
{
auto streamer = Interface<IO::IStreamer>::Get();
m_curReadRequest = streamer->Cancel(m_curReadRequest);
}
}
void AssetDataStream::Seek(AZ::IO::OffsetType bytes, AZ::IO::GenericStream::SeekMode mode)
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzCore);

@ -82,6 +82,10 @@ namespace AZ::Data
//! Gets the size of data loaded (so far).
size_t GetLoadedSize() const { return m_loadedSize; }
//! Request a cancellation of any current IO streamer requests.
//! Note: This is asynchronous and not guaranteed to cancel if the request is already in-process.
void RequestCancel();
private:
//! Perform any operations needed by all variants of Open()
void OpenInternal(size_t assetSize, const char* streamName);

@ -28,6 +28,8 @@ namespace AZ::Data::AssetInternal
class WeakAsset
{
public:
static constexpr bool EnableAssetCancellation = false;
WeakAsset() = default;
WeakAsset(AssetData* assetData, AssetLoadBehavior assetReferenceLoadBehavior);
@ -110,9 +112,16 @@ namespace AZ::Data::AssetInternal
// - If the left side is different than the right, the left side will have one less reference when it gets overwritten
// - If the left and right sides are the same, clearing the right side's reference means one less reference will exist
if (m_assetData)
{
if constexpr (EnableAssetCancellation)
{
m_assetData->ReleaseWeak();
}
else
{
m_assetData->Release();
}
}
m_assetData = AZStd::move(rhs.m_assetData);
rhs.m_assetData = nullptr;
@ -140,15 +149,29 @@ namespace AZ::Data::AssetInternal
m_assetId.SetInvalid();
if (assetData)
{
if constexpr (EnableAssetCancellation)
{
assetData->AcquireWeak();
}
else
{
assetData->Acquire();
}
m_assetId = assetData->GetId();
}
if (m_assetData)
{
if constexpr (EnableAssetCancellation)
{
m_assetData->ReleaseWeak();
}
else
{
m_assetData->Release();
}
}
m_assetData = assetData;
}

@ -2144,7 +2144,7 @@ namespace AZ
if (curIter != m_assetContainers.end())
{
auto newRef = curIter->second.lock();
if (newRef)
if (newRef && newRef->IsValid())
{
return newRef;
}

@ -186,7 +186,8 @@ namespace UnitTest
int GetWeakUseCount() { return m_weakUseCount.load(); }
};
TEST_F(WeakAssetTest, WeakAsset_ConstructionAndDestruction_UpdatesAssetDataWeakRefCount)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable
TEST_F(WeakAssetTest, DISABLED_WeakAsset_ConstructionAndDestruction_UpdatesAssetDataWeakRefCount)
{
TestAssetData testData;
EXPECT_EQ(testData.GetWeakUseCount(), 0);
@ -202,7 +203,8 @@ namespace UnitTest
EXPECT_EQ(testData.GetWeakUseCount(), 0);
}
TEST_F(WeakAssetTest, WeakAsset_MoveOperatorWithDifferentData_UpdatesOldAssetDataWeakRefCount)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable
TEST_F(WeakAssetTest, DISABLED_WeakAsset_MoveOperatorWithDifferentData_UpdatesOldAssetDataWeakRefCount)
{
TestAssetData testData;
EXPECT_EQ(testData.GetWeakUseCount(), 0);
@ -217,7 +219,8 @@ namespace UnitTest
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
}
TEST_F(WeakAssetTest, WeakAsset_MoveOperatorWithSameData_PreservesAssetDataWeakRefCount)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable
TEST_F(WeakAssetTest, DISABLED_WeakAsset_MoveOperatorWithSameData_PreservesAssetDataWeakRefCount)
{
TestAssetData testData;
EXPECT_EQ(testData.GetWeakUseCount(), 0);
@ -234,7 +237,8 @@ namespace UnitTest
AZ_TEST_STOP_TRACE_SUPPRESSION(1);
}
TEST_F(WeakAssetTest, WeakAsset_AssignmentOperator_CopiesDataAndIncrementsWeakRefCount)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable
TEST_F(WeakAssetTest, DISABLED_WeakAsset_AssignmentOperator_CopiesDataAndIncrementsWeakRefCount)
{
TestAssetData testData;
EXPECT_EQ(testData.GetWeakUseCount(), 0);

@ -575,6 +575,91 @@ namespace UnitTest
EXPECT_EQ(baseStatus, expected_base_status);
}
struct DebugListener : AZ::Interface<IDebugAssetEvent>::Registrar
{
void AssetStatusUpdate(AZ::Data::AssetId id, AZ::Data::AssetData::AssetStatus status) override
{
AZ::Debug::Trace::Output(
"", AZStd::string::format("Status %s - %d\n", id.ToString<AZStd::string>().c_str(), static_cast<int>(status)).c_str());
}
void ReleaseAsset(AZ::Data::AssetId id) override
{
AZ::Debug::Trace::Output(
"", AZStd::string::format("Release %s\n", id.ToString<AZStd::string>().c_str()).c_str());
}
};
TEST_F(AssetJobsFloodTest, RapidAcquireAndRelease)
{
DebugListener listener;
auto assetUuids = {
MyAsset1Id,
MyAsset2Id,
MyAsset3Id,
};
AZStd::vector<AZStd::thread> threads;
AZStd::mutex mutex;
AZStd::atomic<int> threadCount(static_cast<int>(assetUuids.size()));
AZStd::condition_variable cv;
AZStd::atomic_bool keepDispatching(true);
auto dispatch = [&keepDispatching]() {
while (keepDispatching)
{
AssetManager::Instance().DispatchEvents();
}
};
AZStd::thread dispatchThread(dispatch);
for (const auto& assetUuid : assetUuids)
{
threads.emplace_back([this, &threadCount, &cv, assetUuid]() {
bool checkLoaded = true;
for (int i = 0; i < 5000; i++)
{
Asset<AssetWithAssetReference> asset1 =
m_testAssetManager->GetAsset(assetUuid, azrtti_typeid<AssetWithAssetReference>(), AZ::Data::AssetLoadBehavior::PreLoad);
if (checkLoaded)
{
asset1.BlockUntilLoadComplete();
EXPECT_TRUE(asset1.IsReady()) << "Iteration " << i << " failed. Asset status: " << static_cast<int>(asset1.GetStatus());
}
checkLoaded = !checkLoaded;
}
--threadCount;
cv.notify_one();
});
}
bool timedOut = false;
// Used to detect a deadlock. If we wait for more than 5 seconds, it's likely a deadlock has occurred
while (threadCount > 0 && !timedOut)
{
AZStd::unique_lock<AZStd::mutex> lock(mutex);
timedOut = (AZStd::cv_status::timeout == cv.wait_until(lock, AZStd::chrono::system_clock::now() + DefaultTimeoutSeconds * 20000));
}
ASSERT_EQ(threadCount, 0) << "Thread count is non-zero, a thread has likely deadlocked. Test will not shut down cleanly.";
for (auto& thread : threads)
{
thread.join();
}
keepDispatching = false;
dispatchThread.join();
AssetManager::Destroy();
}
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetJobsFloodTest, DISABLED_AssetLoadBehaviorIsPreserved)
#else
@ -2592,7 +2677,8 @@ namespace UnitTest
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetManagerCancelTests, DISABLED_CancelLoad_NoReferences_LoadCancels)
#else
TEST_F(AssetManagerCancelTests, CancelLoad_NoReferences_LoadCancels)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable. LYN-3263
TEST_F(AssetManagerCancelTests, DISABLED_CancelLoad_NoReferences_LoadCancels)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
m_assetHandlerAndCatalog->SetArtificialDelayMilliseconds(0, 100);
@ -2632,7 +2718,8 @@ namespace UnitTest
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetManagerCancelTests, DISABLED_CanceledLoad_CanBeLoadedAgainLater)
#else
TEST_F(AssetManagerCancelTests, CanceledLoad_CanBeLoadedAgainLater)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable. LYN-3263
TEST_F(AssetManagerCancelTests, DISABLED_CanceledLoad_CanBeLoadedAgainLater)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
m_assetHandlerAndCatalog->SetArtificialDelayMilliseconds(0, 50);
@ -2681,7 +2768,8 @@ namespace UnitTest
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetManagerCancelTests, DISABLED_CancelLoad_InProgressLoad_Continues)
#else
TEST_F(AssetManagerCancelTests, CancelLoad_InProgressLoad_Continues)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable. LYN-3263
TEST_F(AssetManagerCancelTests, DISABLED_CancelLoad_InProgressLoad_Continues)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
m_assetHandlerAndCatalog->SetArtificialDelayMilliseconds(0, 100);
@ -2903,8 +2991,9 @@ namespace UnitTest
TEST_F(AssetManagerClearAssetReferenceTests,
DISABLED_ContainerLoadTest_AssetLosesAndGainsReferencesDuringLoadAndSuspendedRelease_AssetSuccessfullyFinishesLoading)
#else
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable. LYN-3263
TEST_F(AssetManagerClearAssetReferenceTests,
ContainerLoadTest_AssetLosesAndGainsReferencesDuringLoadAndSuspendedRelease_AssetSuccessfullyFinishesLoading)
DISABLED_ContainerLoadTest_AssetLosesAndGainsReferencesDuringLoadAndSuspendedRelease_AssetSuccessfullyFinishesLoading)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
// Start the load and wait for the dependent asset to hit the loading state.
@ -2961,7 +3050,8 @@ namespace UnitTest
#if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
TEST_F(AssetManagerClearAssetReferenceTests, DISABLED_ContainerLoadTest_RootAssetDestroyedWhileContainerLoading_ContainerFinishesLoad)
#else
TEST_F(AssetManagerClearAssetReferenceTests, ContainerLoadTest_RootAssetDestroyedWhileContainerLoading_ContainerFinishesLoad)
// Asset cancellation is temporarily disabled, re-enable this test when cancellation is more stable. LYN-3263
TEST_F(AssetManagerClearAssetReferenceTests, DISABLED_ContainerLoadTest_RootAssetDestroyedWhileContainerLoading_ContainerFinishesLoad)
#endif // AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
{
OnAssetReadyListener assetStatus1(DependentPreloadAssetId, azrtti_typeid<AssetWithAssetReference>());

@ -31,11 +31,19 @@ namespace AzToolsFramework
//! /param entities The entities to put under the new prefab.
//! /param nestedPrefabInstances The nested prefab instances to put under the new prefab.
//! /param filePath The filepath corresponding to the prefab file to be created.
//! /param instanceToParentUnder The instance under which the newly created prefab instance is parented under.
//! /param instanceToParentUnder The instance the newly created prefab instance is parented under.
//! /return The optional reference to the prefab created.
virtual Prefab::InstanceOptionalReference CreatePrefab(
const AZStd::vector<AZ::Entity*>& entities, AZStd::vector<AZStd::unique_ptr<Prefab::Instance>>&& nestedPrefabInstances,
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder = AZStd::nullopt) = 0;
//! Instantiate the prefab file provided.
//! /param filePath The filepath for the prefab file the instance should be created from.
//! /param instanceToParentUnder The instance the newly instantiated prefab instance is parented under.
//! /return The optional reference to the prefab instance.
virtual Prefab::InstanceOptionalReference InstantiatePrefab(
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder = AZStd::nullopt) = 0;
virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0;
virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0;

@ -19,6 +19,7 @@
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/PrefabDomUtils.h>
#include <AzToolsFramework/Prefab/PrefabLoader.h>
@ -307,6 +308,26 @@ namespace AzToolsFramework
return AZStd::nullopt;
}
Prefab::InstanceOptionalReference PrefabEditorEntityOwnershipService::InstantiatePrefab(
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder)
{
AZStd::unique_ptr<Prefab::Instance> createdPrefabInstance = m_prefabSystemComponent->InstantiatePrefab(filePath);
if (createdPrefabInstance)
{
if (!instanceToParentUnder)
{
instanceToParentUnder = *m_rootInstance;
}
Prefab::Instance& addedInstance = instanceToParentUnder->get().AddInstance(AZStd::move(createdPrefabInstance));
HandleEntitiesAdded({addedInstance.m_containerEntity.get()});
return addedInstance;
}
return AZStd::nullopt;
}
Prefab::InstanceOptionalReference PrefabEditorEntityOwnershipService::GetRootPrefabInstance()
{
AZ_Assert(m_rootInstance, "A valid root prefab instance couldn't be found in PrefabEditorEntityOwnershipService.");

@ -191,6 +191,9 @@ namespace AzToolsFramework
const AZStd::vector<AZ::Entity*>& entities, AZStd::vector<AZStd::unique_ptr<Prefab::Instance>>&& nestedPrefabInstances,
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override;
Prefab::InstanceOptionalReference InstantiatePrefab(
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override;
Prefab::InstanceOptionalReference GetRootPrefabInstance() override;
//////////////////////////////////////////////////////////////////////////

@ -161,6 +161,61 @@ namespace AzToolsFramework
return AZ::Success();
}
PrefabOperationResult PrefabPublicHandler::InstantiatePrefab(
AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position)
{
auto prefabEditorEntityOwnershipInterface = AZ::Interface<PrefabEditorEntityOwnershipInterface>::Get();
if (!prefabEditorEntityOwnershipInterface)
{
return AZ::Failure(AZStd::string("Could not instantiate prefab - internal error "
"(PrefabEditorEntityOwnershipInterface unavailable)."));
}
InstanceOptionalReference instanceToParentUnder;
// Get parent entity and owning instance
if (parent.IsValid())
{
instanceToParentUnder = m_instanceEntityMapperInterface->FindOwningInstance(parent);
}
if (!instanceToParentUnder.has_value())
{
instanceToParentUnder = prefabEditorEntityOwnershipInterface->GetRootPrefabInstance();
parent = instanceToParentUnder->get().GetContainerEntityId();
}
{
// Initialize Undo Batch object
ScopedUndoBatch undoBatch("Instantiate Prefab");
PrefabDom instanceToParentUnderDomBeforeCreate;
m_instanceToTemplateInterface->GenerateDomForInstance(
instanceToParentUnderDomBeforeCreate, instanceToParentUnder->get());
// Instantiate the Prefab
auto instanceToCreate = prefabEditorEntityOwnershipInterface->InstantiatePrefab(filePath, instanceToParentUnder);
if (!instanceToCreate)
{
return AZ::Failure(AZStd::string("Could not instantiate the prefab provided - internal error "
"(A null instance is returned)."));
}
PrefabUndoHelpers::UpdatePrefabInstance(
instanceToParentUnder->get(), "Update prefab instance", instanceToParentUnderDomBeforeCreate, undoBatch.GetUndoBatch());
CreateLink({}, instanceToCreate->get(), instanceToParentUnder->get().GetTemplateId(),
undoBatch.GetUndoBatch(), parent);
AZ::EntityId containerEntityId = instanceToCreate->get().GetContainerEntityId();
// Apply position
AZ::TransformBus::Event(containerEntityId, &AZ::TransformBus::Events::SetWorldTranslation, position);
}
return AZ::Success();
}
PrefabOperationResult PrefabPublicHandler::FindCommonRootOwningInstance(
const AZStd::vector<AZ::EntityId>& entityIds, EntityList& inputEntityList, EntityList& topLevelEntities,
AZ::EntityId& commonRootEntityId, InstanceOptionalReference& commonRootEntityOwningInstance)
@ -222,19 +277,16 @@ namespace AzToolsFramework
m_instanceToTemplateInterface->GeneratePatch(patch, containerEntityDomBefore, containerEntityDomAfter);
m_instanceToTemplateInterface->AppendEntityAliasToPatchPaths(patch, containerEntityId);
PrefabUndoHelpers::CreateLink(
LinkId linkId = PrefabUndoHelpers::CreateLink(
sourceInstance.GetTemplateId(), targetTemplateId, patch, sourceInstance.GetInstanceAlias(),
undoBatch);
sourceInstance.SetLinkId(linkId);
// Update the cache - this prevents these changes from being stored in the regular undo/redo nodes
m_prefabUndoCache.Store(containerEntityId, AZStd::move(containerEntityDomAfter));
}
PrefabOperationResult PrefabPublicHandler::InstantiatePrefab(AZStd::string_view /*filePath*/, AZ::EntityId /*parent*/, AZ::Vector3 /*position*/)
{
return AZ::Failure(AZStd::string("Prefab - InstantiatePrefab is yet to be implemented."));
}
PrefabOperationResult PrefabPublicHandler::SavePrefab(AZ::IO::Path filePath)
{
auto templateId = m_prefabSystemComponentInterface->GetTemplateIdFromFilePath(filePath.c_str());
@ -322,9 +374,9 @@ namespace AzToolsFramework
AZ::EntityId entityId, UndoSystem::URSequencePoint* parentUndoBatch)
{
// Create Undo node on entities if they belong to an instance
InstanceOptionalReference instanceOptionalReference = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
InstanceOptionalReference owningInstance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
if (instanceOptionalReference.has_value())
if (owningInstance.has_value())
{
PrefabDom afterState;
AZ::Entity* entity = GetEntityById(entityId);

@ -45,7 +45,7 @@ namespace AzToolsFramework
// PrefabPublicInterface...
PrefabOperationResult CreatePrefab(const AZStd::vector<AZ::EntityId>& entityIds, AZ::IO::PathView filePath) override;
PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, AZ::Vector3 position) override;
PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override;
PrefabOperationResult SavePrefab(AZ::IO::Path filePath) override;
PrefabEntityResult CreateEntity(AZ::EntityId parentId, const AZ::Vector3& position) override;

@ -58,7 +58,7 @@ namespace AzToolsFramework
* @param position The position in world space the prefab should be instantiated in.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, AZ::Vector3 position) = 0;
virtual PrefabOperationResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) = 0;
/**
* Saves changes to prefab to disk.

@ -261,6 +261,29 @@ namespace AzToolsFramework
}
}
AZStd::unique_ptr<Instance> PrefabSystemComponent::InstantiatePrefab(AZ::IO::PathView filePath)
{
// Retrieve the template id for the source prefab filepath
Prefab::TemplateId templateId = GetTemplateIdFromFilePath(filePath);
if (templateId == Prefab::InvalidTemplateId)
{
// Load the template from the file
templateId = m_prefabLoader.LoadTemplateFromFile(filePath);
}
if (templateId == Prefab::InvalidTemplateId)
{
AZ_Error("Prefab", false,
"Could not load template from path %s during InstantiatePrefab. Unable to proceed",
filePath);
return nullptr;
}
return InstantiatePrefab(templateId);
}
AZStd::unique_ptr<Instance> PrefabSystemComponent::InstantiatePrefab(const TemplateId& templateId)
{
TemplateReference instantiatingTemplate = FindTemplate(templateId);

@ -115,9 +115,16 @@ namespace AzToolsFramework
*/
void RemoveAllTemplates() override;
/**
* Generates a new Prefab Instance based on the Template whose source is stored in filepath.
* @param filePath the path to the prefab source file containing the template being instantiated.
* @return A unique_ptr to the newly instantiated instance. Null if operation failed.
*/
AZStd::unique_ptr<Instance> InstantiatePrefab(AZ::IO::PathView filePath) override;
/**
* Generates a new Prefab Instance based on the Template referenced by templateId
* @param templateId the id of the template being instantiated
* @param templateId the id of the template being instantiated.
* @return A unique_ptr to the newly instantiated instance. Null if operation failed.
*/
AZStd::unique_ptr<Instance> InstantiatePrefab(const TemplateId& templateId) override;

@ -58,6 +58,7 @@ namespace AzToolsFramework
virtual void UpdatePrefabTemplate(TemplateId templateId, const PrefabDom& updatedDom) = 0;
virtual void PropagateTemplateChanges(TemplateId templateId) = 0;
virtual AZStd::unique_ptr<Instance> InstantiatePrefab(AZ::IO::PathView filePath) = 0;
virtual AZStd::unique_ptr<Instance> InstantiatePrefab(const TemplateId& templateId) = 0;
virtual AZStd::unique_ptr<Instance> CreatePrefab(const AZStd::vector<AZ::Entity*>& entities,
AZStd::vector<AZStd::unique_ptr<Instance>>&& instancesToConsume, AZ::IO::PathView filePath,

@ -33,7 +33,7 @@ namespace AzToolsFramework
state->Redo();
}
void CreateLink(
LinkId CreateLink(
TemplateId sourceTemplateId, TemplateId targetTemplateId, PrefabDomReference patch,
const InstanceAlias& instanceAlias, UndoSystem::URSequencePoint* undoBatch)
{
@ -41,6 +41,8 @@ namespace AzToolsFramework
linkAddUndo->Capture(targetTemplateId, sourceTemplateId, instanceAlias, patch, InvalidLinkId);
linkAddUndo->SetParent(undoBatch);
linkAddUndo->Redo();
return linkAddUndo->GetLinkId();
}
void RemoveLink(

@ -21,7 +21,7 @@ namespace AzToolsFramework
void UpdatePrefabInstance(
const Instance& instance, AZStd::string_view undoMessage, const PrefabDom& instanceDomBeforeUpdate,
UndoSystem::URSequencePoint* undoBatch);
void CreateLink(
LinkId CreateLink(
TemplateId sourceTemplateId, TemplateId targetTemplateId, PrefabDomReference patch,
const InstanceAlias& instanceAlias, UndoSystem::URSequencePoint* undoBatch);
void RemoveLink(

@ -1218,13 +1218,18 @@ void EditorViewportWidget::SetViewportId(int id)
CViewport::SetViewportId(id);
// Now that we have an ID, we can initialize our viewport.
m_renderViewport = new AtomToolsFramework::RenderViewportWidget(id, this);
m_defaultViewportContextName = m_renderViewport->GetViewportContext()->GetName();
m_renderViewport = new AtomToolsFramework::RenderViewportWidget(this, false);
if (!m_renderViewport->InitializeViewportContext(id))
{
AZ_Warning("EditorViewportWidget", false, "Failed to initialize RenderViewportWidget's ViewportContext");
return;
}
auto viewportContext = m_renderViewport->GetViewportContext();
m_defaultViewportContextName = viewportContext->GetName();
QBoxLayout* layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this);
layout->setContentsMargins(QMargins());
layout->addWidget(m_renderViewport);
auto viewportContext = m_renderViewport->GetViewportContext();
viewportContext->ConnectViewMatrixChangedHandler(m_cameraViewMatrixChangeHandler);
viewportContext->ConnectProjectionMatrixChangedHandler(m_cameraProjectionMatrixChangeHandler);

@ -18,6 +18,7 @@
#include <SceneAPI/SceneCore/Containers/SceneGraph.h>
#include <SceneAPI/SceneCore/Containers/RuleContainer.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <SceneAPI/SceneCore/Utilities/SceneGraphSelector.h>
namespace AZ
{
@ -50,7 +51,12 @@ namespace AZ
{
AZStd::vector<AZ::Color> clothData;
const char* meshNodeName = graph.GetNodeName(meshNodeIndex).GetPath();
AZStd::string_view meshNodeName = graph.GetNodeName(meshNodeIndex).GetPath();
if (meshNodeName.ends_with(Utilities::OptimizedMeshSuffix))
{
meshNodeName.remove_suffix(Utilities::OptimizedMeshSuffix.size());
}
for (size_t ruleIndex = 0; ruleIndex < rules.GetRuleCount(); ++ruleIndex)
{

@ -47,7 +47,7 @@ namespace AZ
Containers::SceneGraph::NodeIndex SceneGraphSelector::RemapToOptimizedMesh(const Containers::SceneGraph& graph, const Containers::SceneGraph::NodeIndex& index)
{
const auto& nodeName = graph.GetNodeName(index);
const AZStd::string optimizedName = AZStd::string(nodeName.GetPath(), nodeName.GetPathLength()) + "_optimized";
const AZStd::string optimizedName = AZStd::string(nodeName.GetPath(), nodeName.GetPathLength()).append(OptimizedMeshSuffix);
if (auto optimizedIndex = graph.Find(optimizedName); optimizedIndex.IsValid())
{
return optimizedIndex;

@ -22,6 +22,8 @@ namespace AZ::SceneAPI::DataTypes { class ISceneNodeSelectionList; }
namespace AZ::SceneAPI::Utilities
{
inline constexpr AZStd::string_view OptimizedMeshSuffix = "_optimized";
// SceneGraphSelector provides utilities including converting selected and unselected node lists
// in the MeshGroup into the final target node list.
class SceneGraphSelector

@ -70,6 +70,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS)
ly_add_target(
NAME AWSCore.Editor MODULE
NAMESPACE Gem
OUTPUT_SUBDIRECTORY AWSCoreEditorPlugins
FILES_CMAKE
awscore_editor_shared_files.cmake
INCLUDE_DIRECTORIES

@ -1,6 +1,49 @@
# Welcome to the AWS Core Resource Mapping Tool project!
## Setup aws config and credential
Resource mapping tool is using boto3 to interact with aws services:
* Follow boto3
[Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) to setup default aws region.
* Follow boto3
[Credentials](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) to setup default profile or credential keys.
Or follow **AWS CLI** configuration which can be reused by boto3 lib:
* Follow
[Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config)
**In Progress** - Override default aws profile in resource mapping tool
## Python Environment Setup Options
### 1. Engine python environment
In order to use engine python environment, it requires to link Qt binaries for this tool.
Follow cmake instructions to configure your project, for example:
```
$ cmake -B <BUILD_FOLDER> -S . -G "Visual Studio 16 2019" -DLY_3RDPARTY_PATH=<PATH_TO_3RDPARTY> -DLY_PROJECTS=<PROJECT_NAME>
```
Build project with **AWSCore.Editor** target to generate required Qt binaries.
(Or use **Editor** target)
```
$ cmake --build <BUILD_FOLDER> --target AWSCore.Editor --config <CONFIG> -j <NUM_JOBS>
```
Launch resource mapping tool under engine root folder:
#### Windows
##### release mode
```
$ python\python.cmd Gems\AWSCore\Code\Tools\ResourceMappingTool\resource_mapping_tool.py --binaries_path <PATH_TO_BUILD_FOLDER>\bin\profile\AWSCoreEditorPlugins
```
##### debug mode
```
$ python\python.cmd debug Gems\AWSCore\Code\Tools\ResourceMappingTool\resource_mapping_tool.py --binaries_path <PATH_TO_BUILD_FOLDER>\bin\debug\AWSCoreEditorPlugins
```
### 2. Python virtual environment
This project is set up like a standard Python project. The initialization
process also creates a virtualenv within this project, stored under the `.env`
directory. To create the virtualenv it assumes that there is a `python3`
@ -32,28 +75,15 @@ Once the virtualenv is activated, you can install the required dependencies.
$ pip install -r requirements.txt
```
## Setup aws config and credential
Resource mapping tool is using boto3 to interact with aws services:
* Follow boto3
[Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) to setup default aws region.
* Follow boto3
[Credentials](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) to setup default profile or credential keys.
Or follow **AWS CLI** configuration which can be reused by boto3 lib:
* Follow
[Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config)
**In Progress** - Override default aws profile in resource mapping tool
## Launch Options
### 1. Launch Resource Mapping Tool from python directly
#### 2.1 Launch Options
##### 2.1.1 Launch Resource Mapping Tool from python directly
At this point you can launch tool like other standard python project.
```
$ python resource_mapping_tool.py
```
### 2. Launch Resource Mapping Tool from batch script/Editor
##### 2.1.2 Launch Resource Mapping Tool from batch script/Editor
Update `resource_mapping_tool.cmd` with your virtualenv full path.
* **VIRTUALENV_PATH**: Fill this variable with your virtualenv full path.

@ -28,13 +28,12 @@ logger = logging.getLogger(__name__)
class ImportResourcesController(QObject):
add_import_resources = Signal(list)
add_import_resources_sender: Signal = Signal(list)
set_notification_frame_text_sender: Signal = Signal(str)
"""
ImportResourcesController is the place to bind ImportResource view with its
corresponding behavior
TODO: add error handling once it is ready
"""
def __init__(self) -> None:
@ -44,6 +43,8 @@ class ImportResourcesController(QObject):
self._view_manager: ViewManager = ViewManager.get_instance()
# Initialize view and model related references
self._import_resources_page: ImportResourcesPage = self._view_manager.get_import_resources_page()
self.set_notification_frame_text_sender.connect(
self._import_resources_page.notification_frame.set_frame_text_receiver)
self._tree_view: ResourceTreeView = self._import_resources_page.tree_view
self._proxy_model: ResourceProxyModel = self._tree_view.resource_proxy_model
@ -60,11 +61,10 @@ class ImportResourcesController(QObject):
self._proxy_model.deduplicate_selected_import_resources(self._tree_view.selectedIndexes())
if unique_resources:
logger.debug(f"Importing selected resources: {unique_resources} ...")
self.add_import_resources.emit(unique_resources)
self.add_import_resources_sender.emit(unique_resources)
self._back_to_view_edit_page()
else:
self._import_resources_page.set_notification_frame_text(
error_messages.IMPORT_RESOURCES_PAGE_NO_RESOURCES_SELECTED_ERROR_MESSAGE)
self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_NO_RESOURCES_SELECTED_ERROR_MESSAGE)
def _start_search_resources_async(self) -> None:
configuration: Configuration = self._configuration_manager.configuration
@ -77,8 +77,7 @@ class ImportResourcesController(QObject):
async_worker = FunctionWorker(self._request_cfn_resources_callback, configuration.region)
async_worker.signals.result.connect(self._load_cfn_resources_callback)
else:
self._import_resources_page.set_notification_frame_text(
error_messages.IMPORT_RESOURCES_PAGE_SEARCH_VERSION_ERROR_MESSAGE)
self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_SEARCH_VERSION_ERROR_MESSAGE)
return
self._tree_view.reset_view()
@ -100,7 +99,7 @@ class ImportResourcesController(QObject):
resources[stack_name] = resource_type_and_names
return resources
except RuntimeError as e:
self._import_resources_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
def _request_typed_resources_callback(self, region: str) -> List[str]:
resource_type_index: int = self._import_resources_page.typed_resources_combobox.currentIndex()
@ -115,12 +114,11 @@ class ImportResourcesController(QObject):
elif resource_type_index == constants.AWS_RESOURCE_S3_BUCKET_INDEX:
resources = aws_utils.list_s3_buckets(region)
else:
self._import_resources_page.set_notification_frame_text(
error_messages.IMPORT_RESOURCES_PAGE_RESOURCE_TYPE_ERROR_MESSAGE)
self.set_notification_frame_text_sender.emit(error_messages.IMPORT_RESOURCES_PAGE_RESOURCE_TYPE_ERROR_MESSAGE)
return resources
except RuntimeError as e:
self._import_resources_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
def _load_cfn_resources_callback(self, resources: Dict[str, List[BasicResourceAttributes]]) -> None:
if not resources:
@ -179,7 +177,7 @@ class ImportResourcesController(QObject):
def reset_page(self):
"""Reset import resources page to its default state"""
self._tree_view.reset_view()
self._import_resources_page.hide_notification_frame()
self._import_resources_page.notification_frame.setVisible(False)
self._import_resources_page.set_current_main_view_index(ImportResourcesPageConstants.TREE_VIEW_PAGE_INDEX)
self._import_resources_page.typed_resources_combobox.setCurrentIndex(-1)
self._import_resources_page.search_version = None

@ -10,7 +10,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import logging
from PySide2.QtCore import (QCoreApplication, QModelIndex, QObject, Slot)
from PySide2.QtCore import (QCoreApplication, QModelIndex, QObject, Signal, Slot)
from PySide2.QtWidgets import QFileDialog
from typing import (Dict, List)
@ -32,6 +32,9 @@ logger = logging.getLogger(__name__)
class ViewEditController(QObject):
set_notification_frame_text_sender: Signal = Signal(str)
set_notification_page_frame_text_sender: Signal = Signal(str)
"""
ViewEditController is the place to bind ViewEdit view with its
corresponding behavior
@ -45,6 +48,10 @@ class ViewEditController(QObject):
self._view_manager: ViewManager = ViewManager.get_instance()
# Initialize view and model related references
self._view_edit_page: ViewEditPage = self._view_manager.get_view_edit_page()
self.set_notification_frame_text_sender.connect(
self._view_edit_page.notification_frame.set_frame_text_receiver)
self.set_notification_page_frame_text_sender.connect(
self._view_edit_page.notification_page_frame.set_frame_text_receiver)
self._table_view: ResourceTableView = self._view_edit_page.table_view
self._proxy_model: ResourceProxyModel = self._table_view.resource_proxy_model
@ -78,7 +85,7 @@ class ViewEditController(QObject):
return True
except IOError as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
return False
def _convert_and_load_into_model(self, config_file_name: str) -> None:
@ -92,7 +99,7 @@ class ViewEditController(QObject):
resources = json_utils.convert_json_dict_to_resources(self._config_file_json_source)
except (IOError, ValueError, KeyError) as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
self._view_edit_page.set_table_view_page_interactions_enabled(False)
# load resources into model
@ -109,7 +116,7 @@ class ViewEditController(QObject):
new_config_file_path, configuration.account_id, configuration.region)
except IOError as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
return
self._rescan_config_directory()
@ -145,7 +152,7 @@ class ViewEditController(QObject):
self._start_search_config_files_async(new_config_directory)
except RuntimeError as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
def _rescan_config_directory(self) -> None:
configuration: Configuration = self._configuration_manager.configuration
@ -155,14 +162,14 @@ class ViewEditController(QObject):
configuration.config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX)
except FileNotFoundError as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
return
self._configuration_manager.configuration.config_files = config_files
self._view_edit_page.set_config_files(config_files)
def _reset_page(self) -> None:
self._view_edit_page.hide_notification_frame()
self._view_edit_page.notification_frame.setVisible(False)
self._view_edit_page.set_table_view_page_interactions_enabled(True)
def _save_changes(self) -> None:
@ -178,14 +185,14 @@ class ViewEditController(QObject):
self._proxy_model.override_all_resources_status(
ResourceMappingAttributesStatus(ResourceMappingAttributesStatus.SUCCESS_STATUS_VALUE,
[ResourceMappingAttributesStatus.SUCCESS_STATUS_VALUE]))
self._view_edit_page.set_notification_frame_text(
self.set_notification_frame_text_sender.emit(
notification_label_text.VIEW_EDIT_PAGE_SAVING_SUCCEED_MESSAGE.format(config_file))
def _search_complete_callback(self) -> None:
self._reset_page()
def _start_search_config_files_async(self, config_directory: str) -> None:
self._view_edit_page.set_notification_page_text(notification_label_text.NOTIFICATION_LOADING_MESSAGE)
self.set_notification_page_frame_text_sender.emit(notification_label_text.NOTIFICATION_LOADING_MESSAGE)
self._view_edit_page.set_current_main_view_index(ViewEditPageConstants.NOTIFICATION_PAGE_INDEX)
self._config_file_json_source.clear()
self._table_view.reset_view()
@ -202,7 +209,7 @@ class ViewEditController(QObject):
config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX)
except FileNotFoundError as e:
logger.exception(e)
self._view_edit_page.set_notification_frame_text(str(e))
self.set_notification_frame_text_sender.emit(str(e))
def _select_config_file(self) -> None:
if self._view_edit_page.config_file_combobox.currentIndex() == -1:
@ -219,7 +226,7 @@ class ViewEditController(QObject):
self._proxy_model.emit_source_model_layout_changed()
else:
self._view_edit_page.set_table_view_page_interactions_enabled(False)
self._view_edit_page.set_notification_frame_text(
self.set_notification_frame_text_sender.emit(
error_messages.VIEW_EDIT_PAGE_READ_FROM_JSON_FAILED_WITH_UNEXPECTED_FILE_ERROR_MESSAGE.format(config_file_name))
self._view_edit_page.set_current_main_view_index(ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX)
@ -262,13 +269,13 @@ class ViewEditController(QObject):
invalid_details))
invalid_proxy_rows: List[int] = self._proxy_model.map_from_source_rows(list(invalid_sources.keys()))
self._view_edit_page.set_notification_frame_text(
self.set_notification_frame_text_sender.emit(
error_messages.VIEW_EDIT_PAGE_SAVING_FAILED_WITH_INVALID_ROW_ERROR_MESSAGE.format(invalid_proxy_rows))
return False
return True
@Slot(list)
def add_import_resources(self, resources: List[BasicResourceAttributes]) -> None:
def add_import_resources_receiver(self, resources: List[BasicResourceAttributes]) -> None:
resource: BasicResourceAttributes
for resource in resources:
resource_builder: ResourceMappingAttributesBuilder = ResourceMappingAttributesBuilder() \

@ -51,4 +51,5 @@ class ControllerManager(object):
logger.info("Setting up ViewEdit and ImportResource controllers ...")
self._view_edit_controller.setup()
self._import_resources_controller.setup()
self._import_resources_controller.add_import_resources.connect(self._view_edit_controller.add_import_resources)
self._import_resources_controller.add_import_resources_sender.connect(
self._view_edit_controller.add_import_resources_receiver)

@ -25,10 +25,10 @@ VIEW_EDIT_PAGE_FOOTER_AREA_HEIGHT: int = 50
VIEW_EDIT_PAGE_MARGIN_TOPBOTTOM: int = 10
# header area
CONFIG_FILE_LABEL_WIDTH: int = 70
CONFIG_FILE_LABEL_WIDTH: int = 65
CONFIG_FILE_COMBOBOX_WIDTH: int = 250
CONFIG_LOCATION_LABEL_WIDTH: int = 110
CONFIG_LOCATION_TEXT_WIDTH: int = 190
CONFIG_LOCATION_LABEL_WIDTH: int = 100
CONFIG_LOCATION_TEXT_WIDTH: int = 180
HEADER_AREA_SEPARATOR_WIDTH: int = 5
# center area

@ -9,32 +9,56 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from argparse import (ArgumentParser, Namespace)
import logging
import sys
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QApplication
from manager.configuration_manager import ConfigurationManager
from manager.controller_manager import ControllerManager
from manager.thread_manager import ThreadManager
from manager.view_manager import ViewManager
from style import azqtcomponents_resources
from utils import environment_utils
from utils import file_utils
# arguments setup
argument_parser: ArgumentParser = ArgumentParser()
argument_parser.add_argument('--binaries_path', help='Path to QT Binaries necessary for PySide.')
argument_parser.add_argument('--debug', action='store_true', help='Execute on debug mode.')
arguments: Namespace = argument_parser.parse_args()
# logging setup
logging.basicConfig(filename="resource_mapping_tool.log", filemode='w', level=logging.INFO,
logging_level: int = logging.INFO
if arguments.debug:
logging_level = logging.DEBUG
logging_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__),
'resource_mapping_tool.log')
logging.basicConfig(filename=logging_path, filemode='w', level=logging_level,
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%H:%M:%S')
logging.getLogger('boto3').setLevel(logging.CRITICAL)
logging.getLogger('botocore').setLevel(logging.CRITICAL)
logging.getLogger('s3transfer').setLevel(logging.CRITICAL)
logging.getLogger('urllib3').setLevel(logging.CRITICAL)
logger = logging.getLogger(__name__)
if __name__ == "__main__":
if arguments.binaries_path and not environment_utils.is_qt_linked():
logger.info("Setting up Qt environment ...")
environment_utils.setup_qt_environment(arguments.binaries_path)
try:
logger.info("Importing tool required modules ...")
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QApplication
from manager.configuration_manager import ConfigurationManager
from manager.controller_manager import ControllerManager
from manager.thread_manager import ThreadManager
from manager.view_manager import ViewManager
from style import azqtcomponents_resources
except ImportError as e:
logger.error(f"Failed to import module [{e.name}] {e}")
environment_utils.cleanup_qt_environment()
exit(-1)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
app: QApplication = QApplication(sys.argv)
app.aboutToQuit.connect(environment_utils.cleanup_qt_environment)
try:
style_sheet_path: str = file_utils.join_path(file_utils.get_parent_directory_path(__file__),

@ -62,7 +62,8 @@ class TestImportResourcesController(TestCase):
self._mocked_proxy_model: MagicMock = self._mocked_tree_view.resource_proxy_model
self._test_import_resources_controller: ImportResourcesController = ImportResourcesController()
self._test_import_resources_controller.add_import_resources = MagicMock()
self._test_import_resources_controller.add_import_resources_sender = MagicMock()
self._test_import_resources_controller.set_notification_frame_text_sender = MagicMock()
self._test_import_resources_controller.setup()
def test_reset_page_resetting_page_with_expected_state(self) -> None:
@ -116,7 +117,7 @@ class TestImportResourcesController(TestCase):
self._mocked_import_resources_page.typed_resources_search_button.clicked.connect.call_args[0]
mocked_call_args[0]() # triggering search_button connected function
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_tree_view.reset_view.assert_not_called()
self._mocked_import_resources_page.set_current_main_view_index.assert_not_called()
@ -170,7 +171,7 @@ class TestImportResourcesController(TestCase):
mock_aws_utils.list_cloudformation_stacks.assert_called_once_with(
TestImportResourcesController._expected_region)
mock_aws_utils.list_cloudformation_stack_resources.assert_not_called()
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once()
self._mocked_import_resources_page.set_current_main_view_index.assert_called_with(
@ -195,7 +196,7 @@ class TestImportResourcesController(TestCase):
TestImportResourcesController._expected_region)
mock_aws_utils.list_cloudformation_stack_resources.assert_called_once_with(
TestImportResourcesController._expected_cfn_stack_name, TestImportResourcesController._expected_region)
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once()
self._mocked_import_resources_page.set_current_main_view_index.assert_called_with(
@ -258,8 +259,8 @@ class TestImportResourcesController(TestCase):
self._mocked_import_resources_page.cfn_stacks_import_button.clicked.connect.call_args[0]
mocked_call_args[0]() # triggering cfn_stacks_import_button connected function
self._test_import_resources_controller.add_import_resources.emit.assert_not_called()
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.add_import_resources_sender.emit.assert_not_called()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
def test_page_cfn_stacks_import_button_emit_signal_with_expected_resources_and_switch_to_expected_page(self) -> None:
self._mocked_proxy_model.deduplicate_selected_import_resources.return_value = \
@ -268,7 +269,7 @@ class TestImportResourcesController(TestCase):
self._mocked_import_resources_page.cfn_stacks_import_button.clicked.connect.call_args[0]
mocked_call_args[0]() # triggering cfn_stacks_import_button connected function
self._test_import_resources_controller.add_import_resources.emit.assert_called_once_with(
self._test_import_resources_controller.add_import_resources_sender.emit.assert_called_once_with(
[TestImportResourcesController._expected_lambda_resource])
self._mocked_view_manager.switch_to_view_edit_page.assert_called_once()
self._mocked_tree_view.reset_view.assert_called_once()
@ -329,7 +330,7 @@ class TestImportResourcesController(TestCase):
mock_aws_utils.list_lambda_functions.assert_called_once_with(
TestImportResourcesController._expected_region)
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once()
self._mocked_import_resources_page.set_current_main_view_index.assert_called_with(
@ -348,7 +349,7 @@ class TestImportResourcesController(TestCase):
mocked_async_call_args: call = mock_thread_manager.get_instance.return_value.start.call_args[0]
mocked_async_call_args[0].run() # triggering async function
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_proxy_model.emit_source_model_layout_changed.assert_called_once()
self._mocked_import_resources_page.set_current_main_view_index.assert_called_with(
@ -383,8 +384,8 @@ class TestImportResourcesController(TestCase):
self._mocked_import_resources_page.typed_resources_import_button.clicked.connect.call_args[0]
mocked_call_args[0]() # triggering typed_resources_import_button connected function
self._test_import_resources_controller.add_import_resources.emit.assert_not_called()
self._mocked_import_resources_page.set_notification_frame_text.assert_called_once()
self._test_import_resources_controller.add_import_resources_sender.emit.assert_not_called()
self._test_import_resources_controller.set_notification_frame_text_sender.emit.assert_called_once()
def test_page_typed_resources_import_button_emit_signal_with_expected_resources_and_switch_to_expected_page(self) -> None:
self._mocked_proxy_model.deduplicate_selected_import_resources.return_value = \
@ -393,7 +394,7 @@ class TestImportResourcesController(TestCase):
self._mocked_import_resources_page.typed_resources_import_button.clicked.connect.call_args[0]
mocked_call_args[0]() # triggering typed_resources_import_button connected function
self._test_import_resources_controller.add_import_resources.emit.assert_called_once_with(
self._test_import_resources_controller.add_import_resources_sender.emit.assert_called_once_with(
[TestImportResourcesController._expected_lambda_resource])
self._mocked_view_manager.switch_to_view_edit_page.assert_called_once()
self._mocked_tree_view.reset_view.assert_called_once()

@ -62,10 +62,12 @@ class TestViewEditController(TestCase):
self._mocked_proxy_model: MagicMock = self._mocked_table_view.resource_proxy_model
self._test_view_edit_controller: ViewEditController = ViewEditController()
self._test_view_edit_controller.set_notification_frame_text_sender = MagicMock()
self._test_view_edit_controller.set_notification_page_frame_text_sender = MagicMock()
self._test_view_edit_controller.setup()
def test_add_import_resources_expected_resource_gets_loaded_into_model(self) -> None:
self._test_view_edit_controller.add_import_resources([TestViewEditController._expected_resource])
self._test_view_edit_controller.add_import_resources_receiver([TestViewEditController._expected_resource])
self._mocked_proxy_model.add_resource.assert_called_once()
mocked_call_args: call = self._mocked_proxy_model.add_resource.call_args[0] # mock call args index is 0
@ -128,7 +130,7 @@ class TestViewEditController(TestCase):
self._mocked_proxy_model.emit_source_model_layout_changed.assert_not_called()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_view_edit_page.set_table_view_page_interactions_enabled.assert_called_with(False)
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_view_edit_page.set_current_main_view_index.assert_called_with(
ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX)
@ -189,7 +191,7 @@ class TestViewEditController(TestCase):
mock_json_utils.validate_json_dict_according_to_json_schema.assert_called_once_with({})
mock_json_utils.convert_json_dict_to_resources.assert_not_called()
self._mocked_proxy_model.load_resource.assert_not_called()
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_view_edit_page.set_table_view_page_interactions_enabled.assert_called_with(False)
@patch("controller.view_edit_controller.file_utils")
@ -239,7 +241,7 @@ class TestViewEditController(TestCase):
self._mocked_view_edit_page.config_file_combobox.currentText.assert_called_once()
self._mocked_table_view.reset_view.assert_called_once()
self._mocked_proxy_model.override_resource_status.assert_called_once()
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.emit_source_model_layout_changed.assert_has_calls([call(), call()])
self._mocked_view_edit_page.set_current_main_view_index.assert_called_with(
ViewEditPageConstants.TABLE_VIEW_PAGE_INDEX)
@ -275,7 +277,7 @@ class TestViewEditController(TestCase):
mocked_call_args[0]() # triggering config_location_button connected function
mock_file_dialog.getExistingDirectory.assert_called_once()
self._mocked_view_edit_page.set_notification_page_text.assert_called_with(
self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_with(
notification_label_text.NOTIFICATION_LOADING_MESSAGE)
self._mocked_view_edit_page.set_current_main_view_index.assert_called_with(
ViewEditPageConstants.NOTIFICATION_PAGE_INDEX)
@ -303,7 +305,7 @@ class TestViewEditController(TestCase):
expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX)
assert self._mocked_configuration_manager.configuration.config_files == []
self._mocked_view_edit_page.set_config_files.assert_called_with([])
self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with(
self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with(
notification_label_text.NOTIFICATION_LOADING_MESSAGE)
@patch("controller.view_edit_controller.ThreadManager")
@ -325,7 +327,7 @@ class TestViewEditController(TestCase):
expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX)
assert self._mocked_configuration_manager.configuration.config_files == []
self._mocked_view_edit_page.set_config_files.assert_called_with([])
self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with(
self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with(
notification_label_text.NOTIFICATION_LOADING_MESSAGE)
@patch("controller.view_edit_controller.ThreadManager")
@ -348,7 +350,7 @@ class TestViewEditController(TestCase):
expected_new_config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX)
assert self._mocked_configuration_manager.configuration.config_files == expected_new_config_files
self._mocked_view_edit_page.set_config_files.assert_called_with(expected_new_config_files)
self._mocked_view_edit_page.set_notification_page_text.assert_called_once_with(
self._test_view_edit_controller.set_notification_page_frame_text_sender.emit.assert_called_once_with(
notification_label_text.NOTIFICATION_LOADING_MESSAGE)
def test_page_add_row_button_expected_resource_gets_loaded_into_model(self) -> None:
@ -395,7 +397,7 @@ class TestViewEditController(TestCase):
mocked_call_args[0]() # triggering save_changes_button connected function
self._mocked_proxy_model.override_resource_status.assert_called_once()
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.override_all_resources_status.assert_not_called()
@patch("controller.view_edit_controller.json_utils")
@ -451,7 +453,7 @@ class TestViewEditController(TestCase):
mock_json_utils.convert_resources_to_json_dict.assert_called_once()
mock_json_utils.write_into_json_file.assert_called_once_with(
TestViewEditController._expected_config_file_full_path, expected_json_dict)
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
self._mocked_proxy_model.override_all_resources_status.assert_not_called()
def test_page_search_filter_input_invoke_proxy_model_with_expected_filter_text(self) -> None:
@ -498,7 +500,7 @@ class TestViewEditController(TestCase):
mock_file_utils.join_path.assert_called_once()
mock_json_utils.create_empty_resource_mapping_file.assert_called_once()
mock_file_utils.find_files_with_suffix_under_directory.assert_not_called()
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()
@patch("controller.view_edit_controller.file_utils")
def test_page_rescan_button_post_notification_when_find_files_throw_exception(
@ -509,4 +511,4 @@ class TestViewEditController(TestCase):
mocked_call_args[0]() # triggering rescan_button connected function
mock_file_utils.find_files_with_suffix_under_directory.assert_called_once()
self._mocked_view_edit_page.set_notification_frame_text.assert_called_once()
self._test_view_edit_controller.set_notification_frame_text_sender.emit.assert_called_once()

@ -55,5 +55,5 @@ class TestControllerManager(TestCase):
TestControllerManager._expected_controller_manager.setup()
mocked_view_edit_controller.setup.assert_called_once()
mocked_import_resources_controller.setup.assert_called_once()
mocked_import_resources_controller.add_import_resources.connect.assert_called_once_with(
mocked_view_edit_controller.add_import_resources)
mocked_import_resources_controller.add_import_resources_sender.connect.assert_called_once_with(
mocked_view_edit_controller.add_import_resources_receiver)

@ -18,7 +18,7 @@ from manager.view_manager import (ViewManager, ViewManagerConstants)
class TestViewManager(TestCase):
"""
ThreadManager unit test cases
ViewManager unit test cases
"""
_mock_import_resources_page: MagicMock
_mock_view_edit_page: MagicMock
@ -36,6 +36,8 @@ class TestViewManager(TestCase):
main_window_patcher: patch = patch("manager.view_manager.QMainWindow")
cls._mock_main_window = main_window_patcher.start()
window_icon_patcher: patch = patch("manager.view_manager.QPixmap")
window_icon_patcher.start()
stacked_pages_patcher: patch = patch("manager.view_manager.QStackedWidget")
cls._mock_stacked_pages = stacked_pages_patcher.start()

@ -0,0 +1,44 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from typing import List
from unittest import TestCase
from unittest.mock import (ANY, call, MagicMock, patch)
from model import constants
from model.basic_resource_attributes import (BasicResourceAttributes, BasicResourceAttributesBuilder)
from utils import environment_utils
class TestEnvironmentUtils(TestCase):
"""
environment utils unit test cases
"""
def setUp(self) -> None:
os_environ_patcher: patch = patch("os.environ")
self.addCleanup(os_environ_patcher.stop)
self._mock_os_environ: MagicMock = os_environ_patcher.start()
os_pathsep_patcher: patch = patch("os.pathsep")
self.addCleanup(os_pathsep_patcher.stop)
self._mock_os_pathsep: MagicMock = os_pathsep_patcher.start()
def test_setup_qt_environment_global_flag_is_set(self) -> None:
environment_utils.setup_qt_environment("dummy")
self._mock_os_environ.copy.assert_called_once()
self._mock_os_pathsep.join.assert_called_once()
assert environment_utils.is_qt_linked() is True
def test_cleanup_qt_environment_global_flag_is_set(self) -> None:
environment_utils.setup_qt_environment("dummy")
assert environment_utils.is_qt_linked() is True
environment_utils.cleanup_qt_environment()
assert environment_utils.is_qt_linked() is False

@ -30,10 +30,6 @@ class TestFileUtils(TestCase):
self.addCleanup(path_patcher.stop)
self._mock_path: MagicMock = path_patcher.start()
windows_path_patcher: patch = patch("pathlib.WindowsPath")
self.addCleanup(windows_path_patcher.stop)
self._mock_windows_path: MagicMock = windows_path_patcher.start()
def test_check_path_exists_returns_true(self) -> None:
mocked_path: MagicMock = self._mock_path.return_value
mocked_path.exists.return_value = True
@ -105,12 +101,35 @@ class TestFileUtils(TestCase):
assert not actual_files
def test_join_path_return_expected_result(self) -> None:
mocked_windows_path: MagicMock = self._mock_windows_path.return_value
mocked_path: MagicMock = self._mock_path.return_value
expected_join_path_name: str = f"{TestFileUtils._expected_path_name}{TestFileUtils._expected_file_name}"
mocked_windows_path.joinpath.return_value = expected_join_path_name
mocked_path.joinpath.return_value = expected_join_path_name
actual_join_path_name: str = file_utils.join_path(TestFileUtils._expected_path_name,
TestFileUtils._expected_file_name)
self._mock_windows_path.assert_called_once_with(TestFileUtils._expected_path_name)
mocked_windows_path.joinpath.assert_called_once_with(TestFileUtils._expected_file_name)
self._mock_path.assert_called_once_with(TestFileUtils._expected_path_name)
mocked_path.joinpath.assert_called_once_with(TestFileUtils._expected_file_name)
assert actual_join_path_name == expected_join_path_name
def test_normalize_file_path_return_empty_when_input_is_empty(self) -> None:
actual_normalized_path: str = file_utils.normalize_file_path("")
assert actual_normalized_path == ""
def test_normalize_file_path_return_expected_result(self) -> None:
mocked_path: MagicMock = self._mock_path.return_value
expected_resolve_path: str = TestFileUtils._expected_path_name
mocked_path.resolve.return_value = expected_resolve_path
actual_resolve_path: str = file_utils.normalize_file_path("dummy")
self._mock_path.assert_called_once()
mocked_path.resolve.assert_called_once()
assert actual_resolve_path == expected_resolve_path
def test_normalize_file_path_return_empty_when_exception_raised(self) -> None:
mocked_path: MagicMock = self._mock_path.return_value
mocked_path.resolve.side_effect = RuntimeError()
actual_resolve_path: str = file_utils.normalize_file_path("dummy")
self._mock_path.assert_called_once()
mocked_path.resolve.assert_called_once()
assert actual_resolve_path == ""

@ -44,15 +44,26 @@ class AWSConstants(object):
S3_SERVICE_NAME: str = "s3"
def _close_client_connection(client: BaseClient) -> None:
session: boto3.session.Session = client._endpoint.http_session
managers: List[object] = [session._manager, *session._proxy_managers.values()]
for manager in managers:
manager.clear()
def _initialize_boto3_aws_client(service: str, region: str = "") -> BaseClient:
if region:
return boto3.client(service, region_name=region)
boto3_client: BaseClient = boto3.client(service, region_name=region)
else:
return boto3.client(service)
boto3_client: BaseClient = boto3.client(service)
boto3_client.meta.events.register(
f"after-call.{service}.*", lambda **kwargs: _close_client_connection(boto3_client)
)
return boto3_client
def get_default_account_id() -> str:
sts_client: BaseClient = boto3.client(AWSConstants.STS_SERVICE_NAME)
sts_client: BaseClient = _initialize_boto3_aws_client(AWSConstants.STS_SERVICE_NAME)
try:
return sts_client.get_caller_identity()["Account"]
except ClientError as error:
@ -65,7 +76,7 @@ def get_default_region() -> str:
if region:
return region
sts_client: BaseClient = boto3.client(AWSConstants.STS_SERVICE_NAME)
sts_client: BaseClient = _initialize_boto3_aws_client(AWSConstants.STS_SERVICE_NAME)
region = sts_client.meta.region_name
if region:
return region

@ -0,0 +1,70 @@
"""
All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
its licensors.
For complete copyright and license terms please see the LICENSE at the root of this
distribution (the "License"). All use of this software is governed by the License,
or, if provided, by the license below or the license accompanying this file. Do not
remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
import logging
import os
from typing import Dict
from utils import file_utils
"""
Environment Utils provide functions to setup python environment libs for resource mapping tool
"""
logger = logging.getLogger(__name__)
qt_binaries_linked: bool = False
old_os_env: Dict[str, str] = os.environ.copy()
def setup_qt_environment(bin_path: str) -> None:
"""
Setup Qt binaries for o3de python runtime environment
:param bin_path: The path of Qt binaries
"""
if is_qt_linked():
logger.info("Qt binaries have already been linked, skip Qt setup")
return
global old_os_env
old_os_env = os.environ.copy()
binaries_path: str = file_utils.normalize_file_path(bin_path)
os.environ["QT_PLUGIN_PATH"] = binaries_path
path = os.environ['PATH']
new_path = os.pathsep.join([binaries_path, path])
os.environ['PATH'] = new_path
global qt_binaries_linked
qt_binaries_linked = True
def is_qt_linked() -> bool:
"""
Check whether Qt binaries have been linked in o3de python runtime environment
:return: True if Qt binaries have been linked; False if not
"""
return qt_binaries_linked
def cleanup_qt_environment() -> None:
"""
Clean up the linked Qt binaries in o3de python runtime environment
"""
if not is_qt_linked():
logger.info("Qt binaries have not been linked, skip Qt uninstall")
return
global old_os_env
if old_os_env.get("QT_PLUGIN_PATH"):
old_os_env.pop("QT_PLUGIN_PATH")
os.environ = old_os_env
global qt_binaries_linked
qt_binaries_linked = False

@ -20,8 +20,8 @@ path, check file existence, etc
logger = logging.getLogger(__name__)
def check_path_exists(full_path: str) -> bool:
return pathlib.Path(full_path).exists()
def check_path_exists(file_path: str) -> bool:
return pathlib.Path(file_path).exists()
def get_current_directory_path() -> str:
@ -44,6 +44,14 @@ def find_files_with_suffix_under_directory(dir_path: str, suffix: str) -> List[s
return results
def join_path(dir_path: str, file_name: str) -> str:
# TODO: expand usage to support Mac and Linux
return str(pathlib.WindowsPath(dir_path).joinpath(file_name))
def normalize_file_path(file_path: str) -> str:
if file_path:
try:
return str(pathlib.Path(file_path).resolve(True))
except (FileNotFoundError, RuntimeError):
logger.warning(f"Failed to normalize file path {file_path}, return empty string instead")
return ""
def join_path(this_path: str, other_path: str) -> str:
return str(pathlib.Path(this_path).joinpath(other_path))

@ -9,6 +9,7 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
from PySide2.QtCore import Slot
from PySide2.QtGui import (QIcon, QPixmap)
from PySide2.QtWidgets import (QFrame, QHBoxLayout, QLabel, QLayout, QLineEdit, QPushButton,
QSizePolicy, QWidget)
@ -58,5 +59,7 @@ class NotificationFrame(QFrame):
self.setVisible(False)
def set_text(self, text: str) -> None:
@Slot(str)
def set_frame_text_receiver(self, text: str) -> None:
self._title_label.setText(text)
self.setVisible(True)

@ -136,7 +136,6 @@ class ImportResourcesPage(QWidget):
self._back_button.setObjectName("Secondary")
self._back_button.setText(f" {notification_label_text.IMPORT_RESOURCES_PAGE_BACK_TEXT}")
self._back_button.setIcon(QIcon(":/Breadcrumb/img/UI20/Breadcrumb/arrow_left-default.svg"))
self._back_button.setFlat(True)
self._back_button.setMinimumSize(view_size_constants.BACK_BUTTON_WIDTH,
view_size_constants.INTERACTION_COMPONENT_HEIGHT)
header_area_layout.addWidget(self._back_button)
@ -325,6 +324,10 @@ class ImportResourcesPage(QWidget):
def search_version(self) -> str:
return self._search_version
@property
def notification_frame(self) -> NotificationFrame:
return self._notification_frame
@search_version.setter
def search_version(self, new_search_version: str) -> None:
self._search_version = new_search_version
@ -343,10 +346,3 @@ class ImportResourcesPage(QWidget):
def set_current_main_view_index(self, index: int) -> None:
"""Switch main view page based on given index"""
self._stacked_pages.setCurrentIndex(index)
def hide_notification_frame(self) -> None:
self._notification_frame.setVisible(False)
def set_notification_frame_text(self, text: str) -> None:
self._notification_frame.set_text(text)
self._notification_frame.setVisible(True)

@ -410,6 +410,14 @@ class ViewEditPage(QWidget):
def rescan_button(self) -> QPushButton:
return self._rescan_button
@property
def notification_frame(self) -> NotificationFrame:
return self._notification_frame
@property
def notification_page_frame(self) -> NotificationFrame:
return self._notification_page_frame
def set_current_main_view_index(self, index: int) -> None:
"""Switch main view page based on given index"""
if index == ViewEditPageConstants.NOTIFICATION_PAGE_INDEX:
@ -436,11 +444,13 @@ class ViewEditPage(QWidget):
self._config_file_combobox.setCurrentIndex(-1)
if config_files:
self._notification_page_frame.set_text(notification_label_text.VIEW_EDIT_PAGE_SELECT_CONFIG_FILE_MESSAGE)
self._notification_page_frame.set_frame_text_receiver(
notification_label_text.VIEW_EDIT_PAGE_SELECT_CONFIG_FILE_MESSAGE)
self._create_new_button.setVisible(False)
self._rescan_button.setVisible(False)
else:
self._notification_page_frame.set_text(notification_label_text.VIEW_EDIT_PAGE_NO_CONFIG_FILE_FOUND_MESSAGE)
self._notification_page_frame.set_frame_text_receiver(
notification_label_text.VIEW_EDIT_PAGE_NO_CONFIG_FILE_FOUND_MESSAGE)
self._create_new_button.setVisible(True)
self._rescan_button.setVisible(True)
@ -455,16 +465,6 @@ class ViewEditPage(QWidget):
self._config_location_text.setText(elided_text)
self._config_location_text.setToolTip(config_location)
def set_notification_page_text(self, text: str) -> None:
self._notification_page_frame.set_text(text)
def hide_notification_frame(self) -> None:
self._notification_frame.setVisible(False)
def set_notification_frame_text(self, text: str) -> None:
self._notification_frame.set_text(text)
self._notification_frame.setVisible(True)
def set_table_view_page_interactions_enabled(self, enabled: bool) -> None:
self._table_view_page.setEnabled(enabled)
self._save_changes_button.setEnabled(enabled)

@ -159,7 +159,8 @@ void MainCS(uint3 thread_id: SV_DispatchThreadID)
blendWeights.z = InstanceSrg::m_sourceBlendWeights[i * 4 + 2];
blendWeights.w = InstanceSrg::m_sourceBlendWeights[i * 4 + 3];
// When all the blend weights of a vertex are zero it means its data is set by the CPU directly
// [TODO ATOM-15288]
// Temporary workaround. When all the blend weights of a vertex are zero it means its data is set by the CPU directly
// and skinning must be skipped to not overwrite it (e.g. cloth simulation).
if(!any(blendWeights))
{

@ -40,7 +40,6 @@ namespace AZ
const RPI::Cullable& GetCullable() { return m_cullable; }
private:
class MeshLoader
: private Data::AssetBus::Handler
{
@ -84,6 +83,11 @@ namespace AZ
MaterialAssignmentMap m_materialAssignments;
Data::Instance<RPI::Model> m_model;
//! A reference to the original model asset in case it got cloned before creating the model instance.
Data::Asset<RPI::ModelAsset> m_originalModelAsset;
MeshFeatureProcessorInterface::RequiresCloneCallback m_requiresCloningCallback;
Data::Instance<RPI::ShaderResourceGroup> m_shaderResourceGroup;
AZStd::unique_ptr<MeshLoader> m_meshLoader;
RPI::Scene* m_scene = nullptr;
@ -131,16 +135,19 @@ namespace AZ
const Data::Asset<RPI::ModelAsset>& modelAsset,
const MaterialAssignmentMap& materials = {},
bool skinnedMeshWithMotion = false,
bool rayTracingEnabled = true) override;
bool rayTracingEnabled = true,
RequiresCloneCallback requiresCloneCallback = {}) override;
MeshHandle AcquireMesh(
const Data::Asset<RPI::ModelAsset> &modelAsset,
const Data::Instance<RPI::Material>& material,
bool skinnedMeshWithMotion = false,
bool rayTracingEnabled = true) override;
bool rayTracingEnabled = true,
RequiresCloneCallback requiresCloneCallback = {}) override;
bool ReleaseMesh(MeshHandle& meshHandle) override;
MeshHandle CloneMesh(const MeshHandle& meshHandle) override;
Data::Instance<RPI::Model> GetModel(const MeshHandle& meshHandle) const override;
Data::Asset<RPI::ModelAsset> GetModelAsset(const MeshHandle& meshHandle) const override;
void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance<RPI::Material>& material) override;
void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const MaterialAssignmentMap& materials) override;
const MaterialAssignmentMap& GetMaterialAssignmentMap(const MeshHandle& meshHandle) const override;

@ -12,9 +12,12 @@
#pragma once
#include <AzCore/EBus/Event.h>
#include <AzCore/Outcome/Outcome.h>
#include <AzCore/std/functional.h>
#include <Atom/Feature/Material/MaterialAssignment.h>
#include <Atom/RPI.Public/Culling.h>
#include <Atom/RPI.Public/FeatureProcessor.h>
#include <Atom/RPI.Reflect/Model/ModelAsset.h>
#include <Atom/Utils/StableDynamicArray.h>
namespace AZ
@ -32,19 +35,23 @@ namespace AZ
using MeshHandle = StableDynamicArrayHandle<MeshDataInstance>;
using ModelChangedEvent = Event<const Data::Instance<RPI::Model>>;
using RequiresCloneCallback = AZStd::function<bool(const Data::Asset<RPI::ModelAsset>& modelAsset)>;
//! Acquires a model with an optional collection of material assignments.
//! @param requiresCloneCallback The callback indicates whether cloning is required for a given model asset.
virtual MeshHandle AcquireMesh(
const Data::Asset<RPI::ModelAsset>& modelAsset,
const MaterialAssignmentMap& materials = {},
bool skinnedMeshWithMotion = false,
bool rayTracingEnabled = true) = 0;
bool rayTracingEnabled = true,
RequiresCloneCallback requiresCloneCallback = {}) = 0;
//! Acquires a model with a single material applied to all its meshes.
virtual MeshHandle AcquireMesh(
const Data::Asset<RPI::ModelAsset>& modelAsset,
const Data::Instance<RPI::Material>& material,
bool skinnedMeshWithMotion = false,
bool rayTracingEnabled = true) = 0;
bool rayTracingEnabled = true,
RequiresCloneCallback requiresCloneCallback = {}) = 0;
//! Releases the mesh handle
virtual bool ReleaseMesh(MeshHandle& meshHandle) = 0;
//! Creates a new instance and handle of a mesh using an existing MeshId. Currently, this will reset the new mesh to default materials.
@ -52,6 +59,8 @@ namespace AZ
//! Gets the underlying RPI::Model instance for a meshHandle. May be null if the model has not loaded.
virtual Data::Instance<RPI::Model> GetModel(const MeshHandle& meshHandle) const = 0;
//! Gets the underlying RPI::ModelAsset for a meshHandle.
virtual Data::Asset<RPI::ModelAsset> GetModelAsset(const MeshHandle& meshHandle) const = 0;
//! Sets the MaterialAssignmentMap for a meshHandle, using just a single material for the DefaultMaterialAssignmentId.
//! Note if there is already a material assignment map, this will replace the entire map with just a single material.
virtual void SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance<RPI::Material>& material) = 0;
@ -61,6 +70,7 @@ namespace AZ
virtual const MaterialAssignmentMap& GetMaterialAssignmentMap(const MeshHandle& meshHandle) const = 0;
//! Connects a handler to any changes to an RPI::Model. Changes include loading and reloading.
virtual void ConnectModelChangeEventHandler(const MeshHandle& meshHandle, ModelChangedEvent::Handler& handler) = 0;
//! Sets the transform for a given mesh handle.
virtual void SetTransform(const MeshHandle& meshHandle, const Transform& transform,
const Vector3& nonUniformScale = Vector3::CreateOne()) = 0;
@ -72,7 +82,7 @@ namespace AZ
virtual void SetSortKey(const MeshHandle& meshHandle, RHI::DrawItemSortKey sortKey) = 0;
//! Gets the sort key for a given mesh handle.
virtual RHI::DrawItemSortKey GetSortKey(const MeshHandle& meshHandle) = 0;
//! Sets an LOD override for a given mesh handle. This LOD will always be rendered instead being automatitcally determined.
//! Sets an LOD override for a given mesh handle. This LOD will always be rendered instead being automatically determined.
virtual void SetLodOverride(const MeshHandle& meshHandle, RPI::Cullable::LodOverride lodOverride) = 0;
//! Gets the LOD override for a given mesh handle.
virtual RPI::Cullable::LodOverride GetLodOverride(const MeshHandle& meshHandle) = 0;

@ -19,16 +19,10 @@ namespace UnitTest
class MockMeshFeatureProcessor : public AZ::Render::MeshFeatureProcessorInterface
{
public:
MOCK_METHOD3(
AcquireMesh,
MeshHandle(const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZ::Render::MaterialAssignmentMap&, bool));
MOCK_METHOD3(
AcquireMesh,
MeshHandle(
const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZStd::intrusive_ptr<AZ ::RPI::Material>&, bool));
MOCK_METHOD1(ReleaseMesh, bool(MeshHandle&));
MOCK_METHOD1(CloneMesh, MeshHandle(const MeshHandle&));
MOCK_CONST_METHOD1(GetModel, AZStd::intrusive_ptr<AZ::RPI::Model>(const MeshHandle&));
MOCK_CONST_METHOD1(GetModelAsset, AZ::Data::Asset<AZ::RPI::ModelAsset>(const MeshHandle&));
MOCK_CONST_METHOD1(GetMaterialAssignmentMap, const AZ::Render::MaterialAssignmentMap&(const MeshHandle&));
MOCK_METHOD2(ConnectModelChangeEventHandler, void(const MeshHandle&, ModelChangedEvent::Handler&));
MOCK_METHOD3(SetTransform, void(const MeshHandle&, const AZ::Transform&, const AZ::Vector3&));
@ -41,8 +35,8 @@ namespace UnitTest
MOCK_METHOD1(GetSortKey, AZ::RHI::DrawItemSortKey(const MeshHandle&));
MOCK_METHOD2(SetLodOverride, void(const MeshHandle&, AZ::RPI::Cullable::LodOverride));
MOCK_METHOD1(GetLodOverride, AZ::RPI::Cullable::LodOverride(const MeshHandle&));
MOCK_METHOD4(AcquireMesh, MeshHandle (const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZ::Render::MaterialAssignmentMap&, bool, bool));
MOCK_METHOD4(AcquireMesh, MeshHandle (const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZ::Data::Instance<AZ::RPI::Material>&, bool, bool));
MOCK_METHOD5(AcquireMesh, MeshHandle (const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZ::Render::MaterialAssignmentMap&, bool, bool, AZ::Render::MeshFeatureProcessorInterface::RequiresCloneCallback));
MOCK_METHOD5(AcquireMesh, MeshHandle (const AZ::Data::Asset<AZ::RPI::ModelAsset>&, const AZ::Data::Instance<AZ::RPI::Material>&, bool, bool, AZ::Render::MeshFeatureProcessorInterface::RequiresCloneCallback));
MOCK_METHOD2(SetRayTracingEnabled, void (const MeshHandle&, bool));
MOCK_METHOD2(SetVisible, void (const MeshHandle&, bool));
MOCK_METHOD2(SetUseForwardPassIblSpecular, void (const MeshHandle&, bool));

@ -139,7 +139,6 @@ namespace AZ
GetLightDataFromFeatureProcessor();
SetLightBuffersToSRG();
SetLightListToSRG();
SetLightsCountToSRG();
SetConstantdataToSRG();
@ -187,13 +186,6 @@ namespace AZ
}
}
void LightCullingPass::SetLightListToSRG()
{
auto inputIndex = m_shaderResourceGroup->FindShaderInputBufferIndex(AZ::Name("m_lightList"));
[[maybe_unused]] bool succeeded = m_shaderResourceGroup->SetBuffer(inputIndex, m_lightList);
AZ_Assert(succeeded, "SetImage failed for light list");
}
void LightCullingPass::SetLightsCountToSRG()
{
for (auto& elem : m_lightdata)

@ -59,7 +59,6 @@ namespace AZ
void SetLightBuffersToSRG();
void SetLightsCountToSRG();
void SetConstantdataToSRG();
void SetLightListToSRG();
AZ::RHI::Size GetDepthBufferResolution();
float CreateTraceValues(const AZ::Vector2& unprojection);

@ -21,6 +21,8 @@
#include <Atom/RPI.Public/Culling.h>
#include <Atom/Utils/StableDynamicArray.h>
#include <Atom/RPI.Reflect/Model/ModelAssetCreator.h>
#include <AtomCore/Instance/InstanceDatabase.h>
#include <AzCore/Console/IConsole.h>
@ -150,7 +152,8 @@ namespace AZ
const Data::Asset<RPI::ModelAsset>& modelAsset,
const MaterialAssignmentMap& materials,
bool skinnedMeshWithMotion,
bool rayTracingEnabled)
bool rayTracingEnabled,
RequiresCloneCallback requiresCloneCallback)
{
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
@ -166,8 +169,9 @@ namespace AZ
meshDataHandle->m_scene = GetParentScene();
meshDataHandle->m_materialAssignments = materials;
meshDataHandle->m_objectId = m_transformService->ReserveObjectId();
meshDataHandle->m_originalModelAsset = modelAsset;
meshDataHandle->m_requiresCloningCallback = requiresCloneCallback;
meshDataHandle->m_meshLoader = AZStd::make_unique<MeshDataInstance::MeshLoader>(modelAsset, &*meshDataHandle);
return meshDataHandle;
@ -177,13 +181,14 @@ namespace AZ
const Data::Asset<RPI::ModelAsset>& modelAsset,
const Data::Instance<RPI::Material>& material,
bool skinnedMeshWithMotion,
bool rayTracingEnabled)
bool rayTracingEnabled,
RequiresCloneCallback requiresCloneCallback)
{
Render::MaterialAssignmentMap materials;
Render::MaterialAssignment& defaultMaterial = materials[AZ::Render::DefaultMaterialAssignmentId];
defaultMaterial.m_materialInstance = material;
return AcquireMesh(modelAsset, materials, skinnedMeshWithMotion, rayTracingEnabled);
return AcquireMesh(modelAsset, materials, skinnedMeshWithMotion, rayTracingEnabled, requiresCloneCallback);
}
bool MeshFeatureProcessor::ReleaseMesh(MeshHandle& meshHandle)
@ -205,7 +210,7 @@ namespace AZ
{
if (meshHandle.IsValid())
{
MeshHandle clone = AcquireMesh(meshHandle->m_model->GetModelAsset(), meshHandle->m_materialAssignments);
MeshHandle clone = AcquireMesh(meshHandle->m_originalModelAsset, meshHandle->m_materialAssignments);
return clone;
}
return MeshFeatureProcessor::MeshHandle();
@ -216,6 +221,16 @@ namespace AZ
return meshHandle.IsValid() ? meshHandle->m_model : nullptr;
}
Data::Asset<RPI::ModelAsset> MeshFeatureProcessor::GetModelAsset(const MeshHandle& meshHandle) const
{
if (meshHandle.IsValid())
{
return meshHandle->m_originalModelAsset;
}
return {};
}
void MeshFeatureProcessor::SetMaterialAssignmentMap(const MeshHandle& meshHandle, const Data::Instance<RPI::Material>& material)
{
Render::MaterialAssignmentMap materials;
@ -430,7 +445,6 @@ namespace AZ
}
// MeshDataInstance::MeshLoader...
MeshDataInstance::MeshLoader::MeshLoader(const Data::Asset<RPI::ModelAsset>& modelAsset, MeshDataInstance* parent)
: m_modelAsset(modelAsset)
, m_parent(parent)
@ -443,10 +457,13 @@ namespace AZ
return;
}
// Check if the model is in the instance database
// Check if the model is in the instance database and skip the loading process in this case.
// The model asset id is used as instance id to indicate that it is a static and shared.
Data::Instance<RPI::Model> model = Data::InstanceDatabase<RPI::Model>::Instance().Find(Data::InstanceId::CreateFromAssetId(m_modelAsset.GetId()));
if (model)
{
// In case the mesh asset requires instancing (e.g. when containing a cloth buffer), the model will always be cloned and there will not be a
// model instance with the asset id as instance id as searched above.
m_parent->Init(model);
m_modelChangedEvent.Signal(AZStd::move(model));
return;
@ -470,8 +487,35 @@ namespace AZ
void MeshDataInstance::MeshLoader::OnAssetReady(Data::Asset<Data::AssetData> asset)
{
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
Data::Asset<RPI::ModelAsset> modelAsset = asset;
// Assign the fully loaded asset back to the mesh handle to not only hold asset id, but the actual data as well.
m_parent->m_originalModelAsset = asset;
Data::Instance<RPI::Model> model;
// Check if a requires cloning callback got set and if so check if cloning the model asset is requested.
if (m_parent->m_requiresCloningCallback &&
m_parent->m_requiresCloningCallback(modelAsset))
{
// Clone the model asset to force create another model instance.
AZ::Data::AssetId newId(AZ::Uuid::CreateRandom(), /*subId=*/0);
Data::Asset<RPI::ModelAsset> clonedAsset;
if (AZ::RPI::ModelAssetCreator::Clone(modelAsset, clonedAsset, newId))
{
model = RPI::Model::FindOrCreate(clonedAsset);
}
else
{
AZ_Error("MeshDataInstance", false, "Cannot clone model for '%s'. Cloth simulation results won't be individual per entity.", modelAsset->GetName().GetCStr());
model = RPI::Model::FindOrCreate(modelAsset);
}
}
else
{
// Static mesh, no cloth buffer present.
model = RPI::Model::FindOrCreate(modelAsset);
}
Data::Instance<RPI::Model> model = RPI::Model::FindOrCreate(asset);
if (model)
{
m_parent->Init(model);

@ -42,7 +42,9 @@ namespace UnitTest
AZStd::string testFilePath = TestDataFolder + AZStd::string("HelloWorld.txt");
AZ::Outcome<AZStd::string, AZStd::string> outcome = AZ::RHI::LoadFileString(testFilePath.c_str());
EXPECT_TRUE(outcome.IsSuccess());
EXPECT_EQ(AZStd::string("Hello World!"), outcome.GetValue());
auto& str = outcome.GetValue();
str.erase(AZStd::remove(str.begin(), str.end(), '\r'));
EXPECT_EQ(AZStd::string("Hello World!\n"), str);
}
TEST_F(UtilsTests, LoadFileBytes)
@ -50,8 +52,10 @@ namespace UnitTest
AZStd::string testFilePath = TestDataFolder + AZStd::string("HelloWorld.txt");
AZ::Outcome<AZStd::vector<uint8_t>, AZStd::string> outcome = AZ::RHI::LoadFileBytes(testFilePath.c_str());
EXPECT_TRUE(outcome.IsSuccess());
AZStd::string expectedText = "Hello World!";
EXPECT_EQ(AZStd::vector<uint8_t>(expectedText.begin(), expectedText.end()), outcome.GetValue());
AZStd::string expectedText = "Hello World!\n";
auto& str = outcome.GetValue();
str.erase(AZStd::remove(str.begin(), str.end(), '\r'));
EXPECT_EQ(AZStd::vector<uint8_t>(expectedText.begin(), expectedText.end()), str);
}
TEST_F(UtilsTests, LoadFileString_Error_DoesNotExist)

@ -118,6 +118,12 @@ namespace AZ
//! Update View's SRG values and compile. This should only be called once per frame before execute command lists.
void UpdateSrg();
using MatrixChangedEvent = AZ::Event<const AZ::Matrix4x4&>;
//! Notifies consumers when the world to view matrix has changed.
void ConnectWorldToViewMatrixChangedHandler(MatrixChangedEvent::Handler& handler);
//! Notifies consumers when the world to clip matrix has changed.
void ConnectWorldToClipMatrixChangedHandler(MatrixChangedEvent::Handler& handler);
private:
View() = delete;
View(const AZ::Name& name, UsageFlags usage);
@ -182,6 +188,9 @@ namespace AZ
// view class doesn't contain subroutines called at the end of each frame
bool m_worldToClipMatrixChanged = true;
bool m_worldToClipPrevMatrixNeedsUpdate = false;
MatrixChangedEvent m_onWorldToClipMatrixChange;
MatrixChangedEvent m_onWorldToViewMatrixChange;
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(View::UsageFlags);

@ -125,7 +125,9 @@ namespace AZ
AzFramework::WindowSize m_viewportSize;
SizeChangedEvent m_sizeChangedEvent;
MatrixChangedEvent m_viewMatrixChangedEvent;
MatrixChangedEvent::Handler m_onViewMatrixChangedHandler;
MatrixChangedEvent m_projectionMatrixChangedEvent;
MatrixChangedEvent::Handler m_onProjectionMatrixChangedHandler;
SceneChangedEvent m_sceneChangedEvent;
ViewportContextManager* m_manager;
RenderPipelinePtr m_currentPipeline;

@ -61,8 +61,15 @@ namespace AZ
//! Otherwise false is returned and result is left untouched.
bool End(Data::Asset<BufferAsset>& result);
private:
//! Clone the given source buffer asset.
//! @param sourceAsset The source buffer asset to clone.
//! @param clonedResult The resulting, cloned buffer asset.
//! @param inOutLastCreatedAssetId The asset id from the model lod asset that owns the cloned buffer asset. The sub id will be increased and
//! used as the asset id for the cloned asset.
//! @result True in case the asset got cloned successfully, false in case an error happened and the clone process got cancelled.
static bool Clone(const Data::Asset<BufferAsset>& sourceAsset, Data::Asset<BufferAsset>& clonedResult, Data::AssetId& inOutLastCreatedAssetId);
private:
bool ValidateBuffer();
};
} // namespace RPI

@ -41,6 +41,13 @@ namespace AZ
//! Otherwise false is returned and result is left untouched.
bool End(Data::Asset<ModelAsset>& result);
//! Clone the given source model asset.
//! @param sourceAsset The source model asset to clone.
//! @param clonedResult The resulting, cloned model lod asset.
//! @param cloneAssetId The asset id to assign to the cloned model asset
//! @result True in case the asset got cloned successfully, false in case an error happened and the clone process got cancelled.
static bool Clone(const Data::Asset<ModelAsset>& sourceAsset, Data::Asset<ModelAsset>& clonedResult, const Data::AssetId& cloneAssetId);
private:
AZ::Aabb m_modelAabb;
};

@ -75,6 +75,14 @@ namespace AZ
//! Finalizes the ModelLodAsset and assigns ownership of the asset to result if successful, otherwise returns false and result is left untouched.
bool End(Data::Asset<ModelLodAsset>& result);
//! Clone the given source model lod asset.
//! @param sourceAsset The source model lod asset to clone.
//! @param clonedResult The resulting, cloned model lod asset.
//! @param inOutLastCreatedAssetId The asset id from the model asset that owns the cloned model lod asset. The sub id will be increased and
//! used as the asset id for the cloned asset.
//! @result True in case the asset got cloned successfully, false in case an error happened and the clone process got cancelled.
static bool Clone(const Data::Asset<ModelLodAsset>& sourceAsset, Data::Asset<ModelLodAsset>& clonedResult, Data::AssetId& inOutLastCreatedAssetId);
private:
bool m_meshBegan = false;

@ -205,7 +205,7 @@ namespace AZ
const auto isNonOptimizedMesh = [](const SceneAPI::Containers::SceneGraph& graph, SceneAPI::Containers::SceneGraph::NodeIndex& index)
{
return SceneAPI::Utilities::SceneGraphSelector::IsMesh(graph, index) &&
!AZStd::string_view{graph.GetNodeName(index).GetName(), graph.GetNodeName(index).GetNameLength()}.ends_with("_optimized");
!AZStd::string_view{graph.GetNodeName(index).GetName(), graph.GetNodeName(index).GetNameLength()}.ends_with(SceneAPI::Utilities::OptimizedMeshSuffix);
};
if (lodRule)
@ -310,7 +310,16 @@ namespace AZ
// Gather mesh content
SourceMeshContent sourceMesh;
sourceMesh.m_name = meshName;
// Although the nodes used to gather mesh content are the optimized ones (when found), to make
// this process transparent for the end-asset generated, the name assigned to the source mesh
// content will not include the "_optimized" prefix.
AZStd::string_view sourceMeshName = meshName;
if (sourceMeshName.ends_with(SceneAPI::Utilities::OptimizedMeshSuffix))
{
sourceMeshName.remove_suffix(SceneAPI::Utilities::OptimizedMeshSuffix.size());
}
sourceMesh.m_name = sourceMeshName;
const auto node = sceneGraph.Find(meshPath);
sourceMesh.m_worldTransform = AZ::SceneAPI::Utilities::DetermineWorldTransform(scene, node, context.m_group.GetRuleContainerConst());

@ -104,6 +104,9 @@ namespace AZ
m_worldToClipMatrix = m_viewToClipMatrix * m_worldToViewMatrix;
m_worldToClipMatrixChanged = true;
m_onWorldToViewMatrixChange.Signal(m_worldToViewMatrix);
m_onWorldToClipMatrixChange.Signal(m_worldToClipMatrix);
InvalidateSrg();
}
@ -132,6 +135,9 @@ namespace AZ
m_clipToWorldMatrix = m_viewToWorldMatrix * m_clipToViewMatrix;
m_worldToClipMatrixChanged = true;
m_onWorldToViewMatrixChange.Signal(m_worldToViewMatrix);
m_onWorldToClipMatrixChange.Signal(m_worldToClipMatrix);
InvalidateSrg();
}
@ -166,6 +172,8 @@ namespace AZ
m_unprojectionConstants.SetZ(float(-tanHalfFovX));
m_unprojectionConstants.SetW(float(tanHalfFovY));
m_onWorldToClipMatrixChange.Signal(m_worldToClipMatrix);
InvalidateSrg();
}
@ -225,6 +233,16 @@ namespace AZ
passWithDrawListTag->SortDrawList(drawList);
}
void View::ConnectWorldToViewMatrixChangedHandler(View::MatrixChangedEvent::Handler& handler)
{
handler.Connect(m_onWorldToViewMatrixChange);
}
void View::ConnectWorldToClipMatrixChangedHandler(View::MatrixChangedEvent::Handler& handler)
{
handler.Connect(m_onWorldToClipMatrixChange);
}
// [GFX TODO] This function needs unit tests and might need to be reworked
RHI::DrawItemSortKey View::GetSortKeyForPosition(const Vector3& positionInWorld) const
{

@ -34,6 +34,15 @@ namespace AZ
&AzFramework::WindowRequestBus::Events::GetClientAreaSize);
AzFramework::WindowNotificationBus::Handler::BusConnect(nativeWindow);
m_onProjectionMatrixChangedHandler = ViewportContext::MatrixChangedEvent::Handler([this](const AZ::Matrix4x4& matrix)
{
m_projectionMatrixChangedEvent.Signal(matrix);
});
m_onViewMatrixChangedHandler = ViewportContext::MatrixChangedEvent::Handler([this](const AZ::Matrix4x4& matrix)
{
m_projectionMatrixChangedEvent.Signal(matrix);
});
SetRenderScene(renderScene);
}
@ -175,7 +184,6 @@ namespace AZ
void ViewportContext::SetCameraProjectionMatrix(const AZ::Matrix4x4& matrix)
{
GetDefaultView()->SetViewToClipMatrix(matrix);
m_projectionMatrixChangedEvent.Signal(matrix);
}
AZ::Transform ViewportContext::GetCameraTransform() const
@ -192,18 +200,23 @@ namespace AZ
{
const auto view = GetDefaultView();
view->SetCameraTransform(AZ::Matrix3x4::CreateFromTransform(transform.GetOrthogonalized()));
m_viewMatrixChangedEvent.Signal(view->GetWorldToViewMatrix());
}
void ViewportContext::SetDefaultView(ViewPtr view)
{
if (m_defaultView != view)
{
m_onProjectionMatrixChangedHandler.Disconnect();
m_onViewMatrixChangedHandler.Disconnect();
m_defaultView = view;
UpdatePipelineView();
m_viewMatrixChangedEvent.Signal(view->GetWorldToViewMatrix());
m_projectionMatrixChangedEvent.Signal(view->GetViewToClipMatrix());
view->ConnectWorldToViewMatrixChangedHandler(m_onViewMatrixChangedHandler);
view->ConnectWorldToClipMatrixChangedHandler(m_onProjectionMatrixChangedHandler);
}
}

@ -155,5 +155,21 @@ namespace AZ
m_asset.SetHint(name);
}
bool BufferAssetCreator::Clone(const Data::Asset<BufferAsset>& sourceAsset, Data::Asset<BufferAsset>& clonedResult, Data::AssetId& inOutLastCreatedAssetId)
{
BufferAssetCreator creator;
inOutLastCreatedAssetId.m_subId = inOutLastCreatedAssetId.m_subId + 1;
creator.Begin(inOutLastCreatedAssetId);
creator.SetBufferName(sourceAsset.GetHint());
creator.SetUseCommonPool(sourceAsset->GetCommonPoolType());
creator.SetPoolAsset(sourceAsset->GetPoolAsset());
creator.SetBufferViewDescriptor(sourceAsset->GetBufferViewDescriptor());
const AZStd::array_view<uint8_t> sourceBuffer = sourceAsset->GetBuffer();
creator.SetBuffer(sourceBuffer.data(), sourceBuffer.size(), sourceAsset->GetBufferDescriptor());
return creator.End(clonedResult);
}
} // namespace RPI
} // namespace AZ

@ -11,6 +11,7 @@
*/
#include <Atom/RPI.Reflect/Model/ModelAssetCreator.h>
#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
#include <AzCore/Asset/AssetManager.h>
@ -64,5 +65,37 @@ namespace AZ
m_asset->SetReady();
return EndCommon(result);
}
bool ModelAssetCreator::Clone(const Data::Asset<ModelAsset>& sourceAsset, Data::Asset<ModelAsset>& clonedResult, const Data::AssetId& cloneAssetId)
{
if (!sourceAsset.IsReady())
{
return false;
}
ModelAssetCreator creator;
creator.Begin(cloneAssetId);
creator.SetName(sourceAsset->GetName().GetStringView());
AZ::Data::AssetId lastUsedId = cloneAssetId;
const AZStd::array_view<Data::Asset<ModelLodAsset>> sourceLodAssets = sourceAsset->GetLodAssets();
for (const Data::Asset<ModelLodAsset>& sourceLodAsset : sourceLodAssets)
{
Data::Asset<ModelLodAsset> lodAsset;
if (!ModelLodAssetCreator::Clone(sourceLodAsset, lodAsset, lastUsedId))
{
AZ_Error("ModelAssetCreator", false,
"Cannot clone model lod asset for '%s'.", sourceLodAsset.GetHint().c_str());
return false;
}
if (lodAsset.IsReady())
{
creator.AddLodAsset(AZStd::move(lodAsset));
}
}
return creator.End(clonedResult);
}
} // namespace RPI
} // namespace AZ

@ -10,6 +10,8 @@
*
*/
#include <AzCore/std/containers/set.h>
#include <Atom/RPI.Reflect/Buffer/BufferAssetCreator.h>
#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
#include <AzCore/Asset/AssetManager.h>
@ -240,5 +242,88 @@ namespace AZ
return true;
}
bool ModelLodAssetCreator::Clone(const Data::Asset<ModelLodAsset>& sourceAsset, Data::Asset<ModelLodAsset>& clonedResult, Data::AssetId& inOutLastCreatedAssetId)
{
AZStd::array_view<ModelLodAsset::Mesh> sourceMeshes = sourceAsset->GetMeshes();
if (sourceMeshes.empty())
{
return true;
}
ModelLodAssetCreator creator;
inOutLastCreatedAssetId.m_subId = inOutLastCreatedAssetId.m_subId + 1;
creator.Begin(inOutLastCreatedAssetId);
// Add the index buffer
const Data::Asset<BufferAsset> sourceIndexBufferAsset = sourceMeshes[0].GetIndexBufferAssetView().GetBufferAsset();
Data::Asset<BufferAsset> clonedIndexBufferAsset;
BufferAssetCreator::Clone(sourceIndexBufferAsset, clonedIndexBufferAsset, inOutLastCreatedAssetId);
creator.SetLodIndexBuffer(clonedIndexBufferAsset);
// Add meshes
AZStd::unordered_map<AZ::Data::AssetId, Data::Asset<BufferAsset>> oldToNewBufferAssets;
for (const ModelLodAsset::Mesh& sourceMesh : sourceMeshes)
{
// Add stream buffers
for (const AZ::RPI::ModelLodAsset::Mesh::StreamBufferInfo& streamBufferInfo : sourceMesh.GetStreamBufferInfoList())
{
const Data::Asset<BufferAsset>& sourceStreamBuffer = streamBufferInfo.m_bufferAssetView.GetBufferAsset();
const AZ::Data::AssetId sourceBufferAssetId = sourceStreamBuffer.GetId();
// In case the buffer asset id is not part of our old to new asset id mapping, we did not convert and add it yet.
if (oldToNewBufferAssets.find(sourceBufferAssetId) == oldToNewBufferAssets.end())
{
Data::Asset<BufferAsset> streamBufferAsset;
if (!BufferAssetCreator::Clone(sourceStreamBuffer, streamBufferAsset, inOutLastCreatedAssetId))
{
AZ_Error("ModelLodAssetCreator", false,
"Cannot clone buffer asset for '%s'.", sourceBufferAssetId.ToString<AZStd::string>().c_str());
return false;
}
oldToNewBufferAssets[sourceBufferAssetId] = streamBufferAsset;
creator.AddLodStreamBuffer(streamBufferAsset);
}
}
// Add mesh
creator.BeginMesh();
creator.SetMeshName(sourceMesh.GetName());
AZ::Aabb aabb = sourceMesh.GetAabb();
creator.SetMeshAabb(AZStd::move(aabb));
creator.SetMeshMaterialAsset(sourceMesh.GetMaterialAsset());
// Mesh index buffer view
const BufferAssetView& sourceIndexBufferView = sourceMesh.GetIndexBufferAssetView();
BufferAssetView indexBufferAssetView(clonedIndexBufferAsset, sourceIndexBufferView.GetBufferViewDescriptor());
creator.SetMeshIndexBuffer(indexBufferAssetView);
// Mesh stream buffer views
for (const AZ::RPI::ModelLodAsset::Mesh::StreamBufferInfo& streamBufferInfo : sourceMesh.GetStreamBufferInfoList())
{
// Get the corresponding new buffer asset id from the source buffer.
const AZ::Data::AssetId sourceBufferAssetId = streamBufferInfo.m_bufferAssetView.GetBufferAsset().GetId();
const auto assetIdIterator = oldToNewBufferAssets.find(sourceBufferAssetId);
if (assetIdIterator != oldToNewBufferAssets.end())
{
const Data::Asset<BufferAsset>& clonedBufferAsset = assetIdIterator->second;
BufferAssetView bufferAssetView(clonedBufferAsset, streamBufferInfo.m_bufferAssetView.GetBufferViewDescriptor());
creator.AddMeshStreamBuffer(streamBufferInfo.m_semantic, streamBufferInfo.m_customName, bufferAssetView);
}
else
{
AZ_Error("ModelLodAssetCreator", false,
"Cannot find cloned buffer asset for source buffer asset '%s'.",
sourceBufferAssetId.ToString<AZStd::string>().c_str());
return false;
}
}
creator.EndMesh();
}
return creator.End(clonedResult);
}
} // namespace RPI
} // namespace AZ

@ -43,9 +43,16 @@ namespace AtomToolsFramework
//! Creates a RenderViewportWidget.
//! Requires the Atom RPI to be initialized in order
//! to internally construct an RPI::ViewportContext.
explicit RenderViewportWidget(AzFramework::ViewportId id = AzFramework::InvalidViewportId, QWidget* parent = nullptr);
//! If initializeViewportContext is set to false, nothing will be displayed on-screen until InitiliazeViewportContext is called.
explicit RenderViewportWidget(QWidget* parent = nullptr, bool shouldInitializeViewportContext = true);
~RenderViewportWidget();
//! Initializes the underlying ViewportContext, if it hasn't already been.
//! If id is specified, the target ViewportContext will be overridden.
//! NOTE: ViewportContext IDs must be unique.
//! Returns true if the ViewportContext is available
//! (i.e. GetViewportContext will return a valid pointer).
bool InitializeViewportContext(AzFramework::ViewportId id = AzFramework::InvalidViewportId);
//! Gets the name associated with this viewport's ViewportContext.
//! This context name can be used to adjust the current Camera
//! independently of the underlying viewport.

@ -31,12 +31,35 @@
namespace AtomToolsFramework
{
RenderViewportWidget::RenderViewportWidget(AzFramework::ViewportId id, QWidget* parent)
RenderViewportWidget::RenderViewportWidget(QWidget* parent, bool shouldInitializeViewportContext)
: QWidget(parent)
, AzFramework::InputChannelEventListener(AzFramework::InputChannelEventListener::GetPriorityDefault())
{
if (shouldInitializeViewportContext)
{
InitializeViewportContext();
}
setUpdatesEnabled(false);
setFocusPolicy(Qt::FocusPolicy::WheelFocus);
setMouseTracking(true);
}
bool RenderViewportWidget::InitializeViewportContext(AzFramework::ViewportId id)
{
if (m_viewportContext != nullptr)
{
AZ_Assert(id == AzFramework::InvalidViewportId || m_viewportContext->GetId() == id, "Attempted to reinitialize RenderViewportWidget with a different ID");
return true;
}
auto viewportContextManager = AZ::Interface<AZ::RPI::ViewportContextRequestsInterface>::Get();
AZ_Assert(viewportContextManager, "Attempted to construct RenderViewportWidget without ViewportContextManager");
AZ_Assert(viewportContextManager, "Attempted to initialize RenderViewportWidget without ViewportContextManager");
if (viewportContextManager == nullptr)
{
return false;
}
// Before we do anything else, we must create a ViewportContext which will give us a ViewportId if we didn't manually specify one.
AZ::RPI::ViewportContextRequestsInterface::CreationParameters params;
@ -46,6 +69,11 @@ namespace AtomToolsFramework
AzFramework::WindowRequestBus::Handler::BusConnect(params.windowHandle);
m_viewportContext = viewportContextManager->CreateViewportContext(AZ::Name(), params);
if (m_viewportContext == nullptr)
{
return false;
}
SetControllerList(AZStd::make_shared<AzFramework::ViewportControllerList>());
AZ::Name cameraName = AZ::Name(AZStd::string::format("Viewport %i Default Camera", m_viewportContext->GetId()));
@ -58,9 +86,7 @@ namespace AtomToolsFramework
AZ::TickBus::Handler::BusConnect();
AzFramework::WindowRequestBus::Handler::BusConnect(params.windowHandle);
setUpdatesEnabled(false);
setFocusPolicy(Qt::FocusPolicy::WheelFocus);
setMouseTracking(true);
return true;
}
RenderViewportWidget::~RenderViewportWidget()

@ -38,7 +38,7 @@ namespace MaterialEditor
{
MaterialViewportWidget::MaterialViewportWidget(QWidget* parent)
: AtomToolsFramework::RenderViewportWidget(AzFramework::InvalidViewportId, parent)
: AtomToolsFramework::RenderViewportWidget(parent)
, m_ui(new Ui::MaterialViewportWidget)
{
m_ui->setupUi(this);

@ -27,7 +27,7 @@ namespace AZ
{
public:
virtual void SetModelAsset(Data::Asset<RPI::ModelAsset> modelAsset) = 0;
virtual const Data::Asset<RPI::ModelAsset>& GetModelAsset() const = 0;
virtual Data::Asset<const RPI::ModelAsset> GetModelAsset() const = 0;
virtual void SetModelAssetId(Data::AssetId modelAssetId) = 0;
virtual Data::AssetId GetModelAssetId() const = 0;
@ -35,7 +35,7 @@ namespace AZ
virtual void SetModelAssetPath(const AZStd::string& path) = 0;
virtual AZStd::string GetModelAssetPath() const = 0;
virtual const Data::Instance<RPI::Model> GetModel() const = 0;
virtual Data::Instance<RPI::Model> GetModel() const = 0;
virtual void SetSortKey(RHI::DrawItemSortKey sortKey) = 0;
virtual RHI::DrawItemSortKey GetSortKey() const = 0;
@ -78,12 +78,15 @@ namespace AZ
{
AZ::EBusConnectionPolicy<Bus>::Connect(busPtr, context, handler, connectLock, id);
Data::Asset<RPI::ModelAsset> modelAsset;
MeshComponentRequestBus::EventResult(modelAsset, id, &MeshComponentRequestBus::Events::GetModelAsset);
Data::Instance<RPI::Model> model;
MeshComponentRequestBus::EventResult(model, id, &MeshComponentRequestBus::Events::GetModel);
if (model &&
model->GetModelAsset().GetStatus() == AZ::Data::AssetData::AssetStatus::Ready)
modelAsset.GetStatus() == AZ::Data::AssetData::AssetStatus::Ready)
{
handler->OnModelReady(model->GetModelAsset(), model);
handler->OnModelReady(modelAsset, model);
}
}
};

@ -93,7 +93,7 @@ namespace AZ::Render
else
{
debugDisplay.DrawWireDisk(Vector3::CreateZero(), Vector3::CreateAxisZ(), radius);
debugDisplay.DrawArc(Vector3::CreateZero(), radius, 90.0f, 180.0f, -3.0f, 0);
debugDisplay.DrawArc(Vector3::CreateZero(), radius, 270.0f, 180.0f, 3.0f, 0);
debugDisplay.DrawArc(Vector3::CreateZero(), radius, 0.0f, 180.0f, 3.0f, 1);
}
debugDisplay.PopMatrix();

@ -268,12 +268,32 @@ namespace AZ
}
}
bool MeshComponentController::RequiresCloning(const Data::Asset<RPI::ModelAsset>& modelAsset)
{
// Is the model asset containing a cloth buffer? If yes, we need to clone the model asset for instancing.
const AZStd::array_view<AZ::Data::Asset<AZ::RPI::ModelLodAsset>> lodAssets = modelAsset->GetLodAssets();
for (const AZ::Data::Asset<AZ::RPI::ModelLodAsset>& lodAsset : lodAssets)
{
const AZStd::array_view<AZ::RPI::ModelLodAsset::Mesh> meshes = lodAsset->GetMeshes();
for (const AZ::RPI::ModelLodAsset::Mesh& mesh : meshes)
{
if (mesh.GetSemanticBufferAssetView(AZ::Name("CLOTH_DATA")) != nullptr)
{
return true;
}
}
}
return false;
}
void MeshComponentController::HandleModelChange(Data::Instance<RPI::Model> model)
{
if (model)
Data::Asset<RPI::ModelAsset> modelAsset = m_meshFeatureProcessor->GetModelAsset(m_meshHandle);
if (model && modelAsset)
{
m_configuration.m_modelAsset = model->GetModelAsset();
MeshComponentNotificationBus::Event(m_entityId, &MeshComponentNotificationBus::Events::OnModelReady, model->GetModelAsset(), model);
m_configuration.m_modelAsset = modelAsset;
MeshComponentNotificationBus::Event(m_entityId, &MeshComponentNotificationBus::Events::OnModelReady, m_configuration.m_modelAsset, model);
MaterialReceiverNotificationBus::Event(m_entityId, &MaterialReceiverNotificationBus::Events::OnMaterialAssignmentsChanged);
AzFramework::EntityBoundsUnionRequestBus::Broadcast(
&AzFramework::EntityBoundsUnionRequestBus::Events::RefreshEntityLocalBoundsUnion, m_entityId);
@ -288,7 +308,8 @@ namespace AZ
MaterialComponentRequestBus::EventResult(materials, m_entityId, &MaterialComponentRequests::GetMaterialOverrides);
m_meshFeatureProcessor->ReleaseMesh(m_meshHandle);
m_meshHandle = m_meshFeatureProcessor->AcquireMesh(m_configuration.m_modelAsset, materials);
m_meshHandle = m_meshFeatureProcessor->AcquireMesh(m_configuration.m_modelAsset, materials,
/*skinnedMeshWithMotion=*/false, /*rayTracingEnabled=*/true, RequiresCloning);
m_meshFeatureProcessor->ConnectModelChangeEventHandler(m_meshHandle, m_changeEventHandler);
const AZ::Transform& transform = m_transformInterface ? m_transformInterface->GetWorldTM() : AZ::Transform::CreateIdentity();
@ -345,9 +366,9 @@ namespace AZ
}
}
const Data::Asset<RPI::ModelAsset>& MeshComponentController::GetModelAsset() const
Data::Asset<const RPI::ModelAsset> MeshComponentController::GetModelAsset() const
{
return GetModel() ? GetModel()->GetModelAsset() : m_configuration.m_modelAsset;
return m_configuration.m_modelAsset;
}
Data::AssetId MeshComponentController::GetModelAssetId() const
@ -369,7 +390,7 @@ namespace AZ
return assetPathString;
}
const Data::Instance<RPI::Model> MeshComponentController::GetModel() const
Data::Instance<RPI::Model> MeshComponentController::GetModel() const
{
return m_meshFeatureProcessor ? m_meshFeatureProcessor->GetModel(m_meshHandle) : Data::Instance<RPI::Model>();
}

@ -83,17 +83,16 @@ namespace AZ
const MeshComponentConfig& GetConfiguration() const;
private:
AZ_DISABLE_COPY(MeshComponentController);
// MeshComponentRequestBus::Handler overrides ...
void SetModelAsset(Data::Asset<RPI::ModelAsset> modelAsset) override;
const Data::Asset<RPI::ModelAsset>& GetModelAsset() const override;
Data::Asset<const RPI::ModelAsset> GetModelAsset() const override;
void SetModelAssetId(Data::AssetId modelAssetId) override;
Data::AssetId GetModelAssetId() const override;
void SetModelAssetPath(const AZStd::string& modelAssetPath) override;
AZStd::string GetModelAssetPath() const override;
const AZ::Data::Instance<RPI::Model> GetModel() const override;
AZ::Data::Instance<RPI::Model> GetModel() const override;
void SetSortKey(RHI::DrawItemSortKey sortKey) override;
RHI::DrawItemSortKey GetSortKey() const override;
@ -118,6 +117,13 @@ namespace AZ
// MaterialComponentNotificationBus::Handler overrides ...
void OnMaterialsUpdated(const MaterialAssignmentMap& materials) override;
//! Check if the model asset requires to be cloned (e.g. cloth) for unique model instances.
//! @param modelAsset The model asset to check.
//! @result True in case the model asset needs to be cloned before creating the model. False if there is a 1:1 relationship between
//! the model asset and the model and it is static and shared. In the second case the m_originalModelAsset of the mesh handle is
//! equal to the model asset that the model is linked to.
static bool RequiresCloning(const Data::Asset<RPI::ModelAsset>& modelAsset);
void HandleModelChange(Data::Instance<RPI::Model> model);
void RegisterModel();
void UnregisterModel();

@ -253,24 +253,10 @@ namespace AZ
}
}
// If there is cloth data, set all the blend weights to zero to indicate
// the vertices will be updated by cpu.
//
// [TODO ATOM-14478]
// At the moment blend weights is a shared buffer and therefore all
// instances of the actor asset will be affected by it. In the future
// this buffer will be unique per instance and modified by cloth component
// when necessary.
//
// [TODO LYN-1890]
// At the moment, if there is cloth data it is assumed that every vertex in the
// submesh will be simulated by cloth in cpu, so all the weights are set to zero.
// But once the blend weights buffer can be modified per instance, it will be set by
// the cloth component, which decides whether to control the whole submesh or
// to apply an additional simplification pass to remove static triangles from simulation.
// Static triangles are the ones that all its vertices won't move during simulation and
// therefore its weights won't be altered so they are controlled by GPU.
// This additional simplification has been disabled in ClothComponentMesh.cpp for now.
// [TODO ATOM-15288]
// Temporary workaround. If there is cloth data, set all the blend weights to zero to indicate
// the vertices will be updated by cpu. When meshes with cloth data are not dispatched for skinning
// this can be hasClothData can be removed.
// If there is no skinning info, default to 0 weights and display an error
if (hasClothData || !sourceSkinningInfo)
@ -370,14 +356,6 @@ namespace AZ
AZ_Assert(modelLodAsset->GetMeshes().size() > 0, "ModelLod '%d' for model '%s' has 0 meshes", lodIndex, fullFileName.c_str());
const RPI::ModelLodAsset::Mesh& mesh0 = modelLodAsset->GetMeshes()[0];
// Get the amount of vertices and indices
// Get the meshes to process
bool hasUVs = false;
bool hasUVs2 = false;
bool hasTangents = false;
bool hasBitangents = false;
bool hasClothData = false;
// Do a pass over the lod to find the number of sub-meshes, the offset and size of each sub-mesh, and total number of vertices in the lod.
// These will be combined into one input buffer for the source actor, but these offsets and sizes will be used to create multiple sub-meshes for the target skinned actor
uint32_t lodVertexCount = 0;
@ -416,18 +394,18 @@ namespace AZ
const AZ::Vector4* sourceTangents = static_cast<const AZ::Vector4*>(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_TANGENTS));
const AZ::Vector3* sourceBitangents = static_cast<const AZ::Vector3*>(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_BITANGENTS));
const AZ::Vector2* sourceUVs = static_cast<const AZ::Vector2*>(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_UVCOORDS, 0));
const AZ::Vector2* sourceUVs2 = static_cast<const AZ::Vector2*>(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_UVCOORDS, 1));
const uint32_t* sourceClothData = static_cast<uint32_t*>(mesh->FindOriginalVertexData(EMotionFX::Mesh::ATTRIB_CLOTH_DATA));
hasUVs = (sourceUVs != nullptr);
hasUVs2 = (sourceUVs2 != nullptr);
hasTangents = (sourceTangents != nullptr);
hasBitangents = (sourceBitangents != nullptr);
hasClothData = (sourceClothData != nullptr);
const bool hasUVs = (sourceUVs != nullptr);
const bool hasTangents = (sourceTangents != nullptr);
const bool hasBitangents = (sourceBitangents != nullptr);
// For each sub-mesh within each mesh, we want to create a separate sub-piece.
const size_t numSubMeshes = mesh->GetNumSubMeshes();
AZ_Assert(numSubMeshes == modelLodAsset->GetMeshes().size(),
"Number of submeshes (%d) in EMotionFX mesh (lod %d and joint index %d) doesn't match the number of meshes (%d) in model lod asset",
numSubMeshes, lodIndex, jointIndex, modelLodAsset->GetMeshes().size());
for (size_t subMeshIndex = 0; subMeshIndex < numSubMeshes; ++subMeshIndex)
{
const EMotionFX::SubMesh* subMesh = mesh->GetSubMesh(subMeshIndex);
@ -466,6 +444,9 @@ namespace AZ
}
}
// Check if the model mesh asset has cloth data. One ModelLodAsset::Mesh corresponds to one EMotionFX::SubMesh.
const bool hasClothData = modelLodAsset->GetMeshes()[subMeshIndex].GetSemanticBufferAssetView(AZ::Name("CLOTH_DATA")) != nullptr;
ProcessSkinInfluences(mesh, subMesh, vertexBufferOffset, blendIndexBufferData, blendWeightBufferData, hasClothData);
// Increment offsets so that the next sub-mesh can start at the right place

@ -219,7 +219,7 @@ namespace AZ
AZ_Assert(false, "AtomActorInstance::SetModelAsset not supported");
}
const Data::Asset<RPI::ModelAsset>& AtomActorInstance::GetModelAsset() const
Data::Asset<const RPI::ModelAsset> AtomActorInstance::GetModelAsset() const
{
AZ_Assert(GetActor(), "Expecting a Atom Actor Instance having a valid Actor.");
return GetActor()->GetMeshAsset();
@ -253,7 +253,7 @@ namespace AZ
return GetModelAsset().GetHint();
}
const AZ::Data::Instance<RPI::Model> AtomActorInstance::GetModel() const
AZ::Data::Instance<RPI::Model> AtomActorInstance::GetModel() const
{
return m_skinnedMeshInstance->m_model;
}
@ -459,7 +459,7 @@ namespace AZ
MeshComponentRequestBus::Handler::BusConnect(m_entityId);
const Data::Instance<RPI::Model> model = m_meshFeatureProcessor->GetModel(*m_meshHandle);
MeshComponentNotificationBus::Event(m_entityId, &MeshComponentNotificationBus::Events::OnModelReady, model->GetModelAsset(), model);
MeshComponentNotificationBus::Event(m_entityId, &MeshComponentNotificationBus::Events::OnModelReady, GetModelAsset(), model);
}
void AtomActorInstance::UnregisterActor()
@ -485,7 +485,7 @@ namespace AZ
{
// Last boolean parameter indicates if motion vector is enabled
m_meshHandle = AZStd::make_shared<MeshFeatureProcessorInterface::MeshHandle>(
m_meshFeatureProcessor->AcquireMesh(m_skinnedMeshInstance->m_model->GetModelAsset(), materials, true));
m_meshFeatureProcessor->AcquireMesh(m_skinnedMeshInstance->m_model->GetModelAsset(), materials, /*skinnedMeshWithMotion=*/true));
}
// If render proxies already exist, they will be auto-freed
@ -512,10 +512,9 @@ namespace AZ
MaterialReceiverNotificationBus::Event(m_entityId, &MaterialReceiverNotificationBus::Events::OnMaterialAssignmentsChanged);
RegisterActor();
// [TODO ATOM-14478, LYN-1890]
// [TODO ATOM-15288]
// Temporary workaround for cloth to make sure the output skinned buffers are filled at least once.
// When the blend weights buffer can be unique per instance and updated by cloth component,
// FillSkinnedMeshInstanceBuffers can be removed.
// When meshes with cloth data are not dispatched for skinning FillSkinnedMeshInstanceBuffers can be removed.
FillSkinnedMeshInstanceBuffers();
}
else

@ -128,12 +128,12 @@ namespace AZ
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MeshComponentRequestBus::Handler overrides...
void SetModelAsset(Data::Asset<RPI::ModelAsset> modelAsset) override;
const Data::Asset<RPI::ModelAsset>& GetModelAsset() const override;
Data::Asset<const RPI::ModelAsset> GetModelAsset() const override;
void SetModelAssetId(Data::AssetId modelAssetId) override;
Data::AssetId GetModelAssetId() const override;
void SetModelAssetPath(const AZStd::string& modelAssetPath) override;
AZStd::string GetModelAssetPath() const override;
const AZ::Data::Instance<RPI::Model> GetModel() const override;
AZ::Data::Instance<RPI::Model> GetModel() const override;
void SetSortKey(RHI::DrawItemSortKey sortKey) override;
RHI::DrawItemSortKey GetSortKey() const override;
void SetLodOverride(RPI::Cullable::LodOverride lodOverride) override;

@ -59,22 +59,33 @@ namespace AZ
const AZ::Name contextName = atomViewportRequests->GetDefaultViewportContextName();
AZ::RPI::ViewportContextNotificationBus::Handler::BusConnect(contextName);
m_initialized = false;
InitializeViewportSizeIfNeeded();
}
void ImguiAtomSystemComponent::Deactivate()
{
ImGui::OtherActiveImGuiRequestBus::Handler::BusDisconnect();
AZ::RPI::ViewportContextNotificationBus::Handler::BusDisconnect();
}
void ImguiAtomSystemComponent::InitializeViewportSizeIfNeeded()
{
#if defined(IMGUI_ENABLED)
ImGui::ImGuiManagerListenerBus::Broadcast(&ImGui::IImGuiManagerListener::SetResolutionMode, ImGui::ImGuiResolutionMode::LockToResolution);
if (m_initialized)
{
return;
}
auto atomViewportRequests = AZ::Interface<AZ::RPI::ViewportContextRequestsInterface>::Get();
auto defaultViewportContext = atomViewportRequests->GetDefaultViewportContext();
if (defaultViewportContext)
{
// If this succeeds, m_initialized will be set to true.
OnViewportSizeChanged(defaultViewportContext->GetViewportSize());
}
#endif
}
void ImguiAtomSystemComponent::Deactivate()
{
ImGui::OtherActiveImGuiRequestBus::Handler::BusDisconnect();
AZ::RPI::ViewportContextNotificationBus::Handler::BusDisconnect();
}
void ImguiAtomSystemComponent::RenderImGuiBuffers(const ImDrawData& drawData)
{
Render::ImGuiSystemRequestBus::Broadcast(&Render::ImGuiSystemRequests::RenderImGuiBuffersToCurrentViewport, drawData);
@ -83,6 +94,7 @@ namespace AZ
void ImguiAtomSystemComponent::OnRenderTick()
{
#if defined(IMGUI_ENABLED)
InitializeViewportSizeIfNeeded();
ImGui::ImGuiManagerListenerBus::Broadcast(&ImGui::IImGuiManagerListener::Render);
#endif
}
@ -90,7 +102,17 @@ namespace AZ
void ImguiAtomSystemComponent::OnViewportSizeChanged(AzFramework::WindowSize size)
{
#if defined(IMGUI_ENABLED)
ImGui::ImGuiManagerListenerBus::Broadcast(&ImGui::IImGuiManagerListener::SetImGuiRenderResolution, ImVec2{aznumeric_cast<float>(size.m_width), aznumeric_cast<float>(size.m_height)});
ImGui::ImGuiManagerListenerBus::Broadcast([this, size](ImGui::ImGuiManagerListenerBus::Events* imgui)
{
imgui->OverrideRenderWindowSize(size.m_width, size.m_height);
// ImGuiManagerListenerBus may not have been connected when this system component is activated
// as ImGuiManager is not part of a system component we can require and instead just listens for ESYSTEM_EVENT_GAME_POST_INIT.
// Let our ImguiAtomSystemComponent know once we successfully connect and update the viewport size.
if (!m_initialized)
{
m_initialized = true;
}
});
#endif
}
}

@ -48,6 +48,7 @@ namespace AZ
void Deactivate() override;
private:
void InitializeViewportSizeIfNeeded();
// OtherActiveImGuiRequestBus overrides ...
void RenderImGuiBuffers(const ImDrawData& drawData) override;
@ -57,6 +58,7 @@ namespace AZ
void OnViewportSizeChanged(AzFramework::WindowSize size) override;
DebugConsole m_debugConsole;
bool m_initialized = false;
};
} // namespace LYIntegration
} // namespace AZ

@ -98,7 +98,7 @@ namespace Blast
// ActorRenderManager::OnActorCreated
{
EXPECT_CALL(
*m_mockMeshFeatureProcessor, AcquireMesh(_, testing::A<const AZ::Render::MaterialAssignmentMap&>(), _, _))
*m_mockMeshFeatureProcessor, AcquireMesh(_, testing::A<const AZ::Render::MaterialAssignmentMap&>(), _, _, _))
.Times(aznumeric_cast<int>(m_actorFactory->m_mockActors[0]->GetChunkIndices().size()))
.WillOnce(Return(testing::ByMove(AZ::Render::MeshFeatureProcessorInterface::MeshHandle())))
.WillOnce(Return(testing::ByMove(AZ::Render::MeshFeatureProcessorInterface::MeshHandle())));

@ -177,7 +177,7 @@ namespace CommandSystem
}
// Update unique datas for all anim graph instances using the given motion set.
EMotionFX::GetAnimGraphManager().UpdateInstancesUniqueDataUsingMotionSet(motionSet);
EMotionFX::GetAnimGraphManager().InvalidateInstanceUniqueDataUsingMotionSet(motionSet);
// Mark the workspace as dirty
mOldWorkspaceDirtyFlag = GetCommandManager()->GetWorkspaceDirtyFlag();
@ -261,6 +261,11 @@ namespace CommandSystem
GetCommandManager()->ExecuteCommandInsideCommand(commandString, outResult);
}
// Update unique datas for all anim graph instances using the given motion set.
// After removing a motion set, the used motion set from an anim graph instance will be reset. If we call this function after
// RemoveMotionSet, the anim graph instance would hold a nullptr for motion set, and wouldn't be invalidated.
EMotionFX::GetAnimGraphManager().InvalidateInstanceUniqueDataUsingMotionSet(motionSet);
// Destroy the motion set.
EMotionFX::GetMotionManager().RemoveMotionSet(motionSet, true);
@ -278,9 +283,6 @@ namespace CommandSystem
animGraph->RecursiveReinit();
}
// Update unique datas for all anim graph instances using the given motion set.
EMotionFX::GetAnimGraphManager().UpdateInstancesUniqueDataUsingMotionSet(motionSet);
// Mark the workspace as dirty.
mOldWorkspaceDirtyFlag = GetCommandManager()->GetWorkspaceDirtyFlag();
GetCommandManager()->SetWorkspaceDirtyFlag(true);
@ -487,7 +489,7 @@ namespace CommandSystem
}
// Update unique datas for all anim graph instances using the given motion set.
EMotionFX::GetAnimGraphManager().UpdateInstancesUniqueDataUsingMotionSet(motionSet);
EMotionFX::GetAnimGraphManager().InvalidateInstanceUniqueDataUsingMotionSet(motionSet);
// Return the id of the newly created motion set.
AZStd::to_string(outResult, motionSet->GetID());
@ -610,7 +612,7 @@ namespace CommandSystem
}
// Update unique datas for all anim graph instances using the given motion set.
EMotionFX::GetAnimGraphManager().UpdateInstancesUniqueDataUsingMotionSet(motionSet);
EMotionFX::GetAnimGraphManager().InvalidateInstanceUniqueDataUsingMotionSet(motionSet);
// Check if we were able to remove all requested motion entries.
if (!failedToRemoveMotionIdsString.empty())
@ -806,7 +808,7 @@ namespace CommandSystem
}
// Update unique datas for all anim graph instances using the given motion set.
EMotionFX::GetAnimGraphManager().UpdateInstancesUniqueDataUsingMotionSet(motionSet);
EMotionFX::GetAnimGraphManager().InvalidateInstanceUniqueDataUsingMotionSet(motionSet);
// Set the dirty flag.
const AZStd::string command = AZStd::string::format("AdjustMotionSet -motionSetID %i -dirtyFlag true", motionSetID);

@ -156,6 +156,7 @@ namespace EMotionFX
result->mRetargetRootNode = mRetargetRootNode;
result->mInvBindPoseTransforms = mInvBindPoseTransforms;
result->m_optimizeSkeleton = m_optimizeSkeleton;
result->m_skinToSkeletonIndexMap = m_skinToSkeletonIndexMap;
result->RecursiveAddDependencies(this);
@ -1490,18 +1491,19 @@ namespace EMotionFX
const bool skinMetaAssetExists = DoesSkinMetaAssetExist(meshAssetId);
const bool morphTargetMetaAssetExists = DoesMorphTargetMetaAssetExist(m_meshAsset.GetId());
m_skinToSkeletonIndexMap.clear();
// Skin and morph target meta assets are ready, fill the runtime mesh data.
if ((!skinMetaAssetExists || m_skinMetaAsset.IsReady()) &&
(!morphTargetMetaAssetExists || m_morphTargetMetaAsset.IsReady()))
{
// Optional, not all actors have a skinned meshes.
AZStd::unordered_map<AZ::u16, AZ::u16> skinToSkeletonIndexMap;
if (skinMetaAssetExists)
{
skinToSkeletonIndexMap = ConstructSkinToSkeletonIndexMap(m_skinMetaAsset);
m_skinToSkeletonIndexMap = ConstructSkinToSkeletonIndexMap(m_skinMetaAsset);
}
ConstructMeshes(skinToSkeletonIndexMap);
ConstructMeshes(m_skinToSkeletonIndexMap);
// Optional, not all actors have morph targets.
if (morphTargetMetaAssetExists)
@ -3012,17 +3014,22 @@ namespace EMotionFX
jointInfo.mStack->InsertDeformer(/*deformerPosition=*/0, morphTargetDeformer);
}
// The lod has shared buffers that combine the data from each submesh. These buffers can be accessed through the first submesh in their entirety
const AZ::RPI::ModelLodAsset::Mesh& sourceMesh = sourceMeshes[0];
// The lod has shared buffers that combine the data from each submesh. In case any of the submeshes has a
// morph target buffer view we can access the entire morph target buffer via the buffer asset.
AZStd::array_view<uint8_t> morphTargetDeltaView;
for (const AZ::RPI::ModelLodAsset::Mesh& sourceMesh : sourceMeshes)
{
if (const auto* bufferAssetView = sourceMesh.GetSemanticBufferAssetView(AZ::Name("MORPHTARGET_VERTEXDELTAS")))
{
if (const auto* bufferAsset = bufferAssetView->GetBufferAsset().Get())
{
// The buffer of the view is the buffer of the whole LOD, not just the source mesh.
morphTargetDeltaView = bufferAsset->GetBuffer();
break;
}
}
}
AZ_Assert(morphTargetDeltaView.data(), "Unable to find MORPHTARGET_VERTEXDELTAS buffer");
const AZ::RPI::PackedCompressedMorphTargetDelta* vertexDeltas = reinterpret_cast<const AZ::RPI::PackedCompressedMorphTargetDelta*>(morphTargetDeltaView.data());

@ -893,6 +893,8 @@ namespace EMotionFX
const AZ::Data::Asset<AZ::RPI::SkinMetaAsset>& GetSkinMetaAsset() const { return m_skinMetaAsset; }
const AZ::Data::Asset<AZ::RPI::MorphTargetMetaAsset>& GetMorphTargetMetaAsset() const { return m_morphTargetMetaAsset; }
const AZStd::unordered_map<AZ::u16, AZ::u16>& GetSkinToSkeletonIndexMap() const { return m_skinToSkeletonIndexMap; }
void SetMeshAsset(AZ::Data::Asset<AZ::RPI::ModelAsset> asset) { m_meshAsset = asset; }
void SetSkinMetaAsset(AZ::Data::Asset<AZ::RPI::SkinMetaAsset> asset) { m_skinMetaAsset = asset; }
void SetMorphTargetMetaAsset(AZ::Data::Asset<AZ::RPI::MorphTargetMetaAsset> asset) { m_morphTargetMetaAsset = asset; }
@ -954,6 +956,7 @@ namespace EMotionFX
AZ::Data::Asset<AZ::RPI::SkinMetaAsset> m_skinMetaAsset;
AZ::Data::Asset<AZ::RPI::MorphTargetMetaAsset> m_morphTargetMetaAsset;
AZStd::recursive_mutex m_mutex;
AZStd::unordered_map<AZ::u16, AZ::u16> m_skinToSkeletonIndexMap; //!< Mapping joint indices in skin metadata to skeleton indices.
static AZ::Data::AssetId ConstructSkinMetaAssetId(const AZ::Data::AssetId& meshAssetId);
static bool DoesSkinMetaAssetExist(const AZ::Data::AssetId& meshAssetId);

@ -327,7 +327,7 @@ namespace EMotionFX
}
}
void AnimGraphManager::UpdateInstancesUniqueDataUsingMotionSet(EMotionFX::MotionSet* motionSet)
void AnimGraphManager::InvalidateInstanceUniqueDataUsingMotionSet(EMotionFX::MotionSet* motionSet)
{
// Update unique datas for all anim graph instances that use the given motion set.
for (EMotionFX::AnimGraphInstance* animGraphInstance : mAnimGraphInstances)

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

Loading…
Cancel
Save