From ed5ad91a32cc6b4d01410a86e12f20ba6ee42243 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 7 Feb 2026 00:46:32 +0100 Subject: [PATCH] Optimize `SkinnedModel::GetSkeletonMapping` to not use locking for better perf when multi-threading #3827 --- Source/Engine/Content/Assets/SkinnedModel.cpp | 27 ++++++++++++------- Source/Engine/Content/Assets/SkinnedModel.h | 8 +++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index ed41c4aab..ce6300b17 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -61,16 +61,24 @@ Array SkinnedModel::GetBlendShapes() SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bool autoRetarget) { + // Fast-path to use cached mapping SkeletonMapping mapping; mapping.TargetSkeleton = this; + SkeletonMappingData mappingData; + if (_skeletonMappingCache.TryGet(source, mappingData)) + { + mapping.SourceSkeleton = mappingData.SourceSkeleton; + mapping.NodesMapping = mappingData.NodesMapping; + return mapping; + } + mapping.SourceSkeleton = nullptr; + if (WaitForLoaded() || !source || source->WaitForLoaded()) return mapping; + PROFILE_CPU(); ScopeLock lock(Locker); - SkeletonMappingData mappingData; if (!_skeletonMappingCache.TryGet(source, mappingData)) { - PROFILE_CPU(); - // Initialize the mapping SkeletonRetarget* retarget = nullptr; const Guid sourceId = source->GetID(); @@ -823,13 +831,13 @@ bool SkinnedModel::SaveMesh(WriteStream& stream, const ModelData& modelData, int void SkinnedModel::ClearSkeletonMapping() { - for (auto& e : _skeletonMappingCache) + for (const auto& e : _skeletonMappingCache) { e.Key->OnUnloaded.Unbind(this); #if USE_EDITOR e.Key->OnReloading.Unbind(this); #endif - Allocator::Free(e.Value.NodesMapping.Get()); + Allocator::Free((void*)e.Value.NodesMapping.Get()); } _skeletonMappingCache.Clear(); } @@ -837,8 +845,9 @@ void SkinnedModel::ClearSkeletonMapping() void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) { ScopeLock lock(Locker); - auto i = _skeletonMappingCache.Find(obj); - ASSERT(i != _skeletonMappingCache.End()); + SkeletonMappingData mappingData; + bool found = _skeletonMappingCache.TryGet(obj, mappingData); + ASSERT(found); // Unlink event obj->OnUnloaded.Unbind(this); @@ -847,8 +856,8 @@ void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) #endif // Clear cache - Allocator::Free(i->Value.NodesMapping.Get()); - _skeletonMappingCache.Remove(i); + Allocator::Free(mappingData.NodesMapping.Get()); + _skeletonMappingCache.Remove(obj); } uint64 SkinnedModel::GetMemoryUsage() const diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h index 894a080c4..111d4d6cb 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.h +++ b/Source/Engine/Content/Assets/SkinnedModel.h @@ -3,7 +3,7 @@ #pragma once #include "ModelBase.h" -#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Threading/ConcurrentDictionary.h" #include "Engine/Graphics/Models/SkinnedMesh.h" #include "Engine/Graphics/Models/SkeletonData.h" @@ -101,9 +101,9 @@ public: struct FLAXENGINE_API SkeletonMapping { // Target skeleton. - AssetReference TargetSkeleton; + SkinnedModel* TargetSkeleton; // Source skeleton. - AssetReference SourceSkeleton; + SkinnedModel* SourceSkeleton; // The node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each item is index of the source skeleton node into target skeleton node. Span NodesMapping; }; @@ -115,7 +115,7 @@ private: Span NodesMapping; }; - Dictionary _skeletonMappingCache; + ConcurrentDictionary _skeletonMappingCache; public: ///