From 7b7a92758ff1a6f040c535b8e5a8858dd36d7380 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 18:01:47 +0100 Subject: [PATCH] Optimize `VariantType` to use static type name in game or from non-reloadable assemblies This avoids many dynamic memory allocations in Visual Scripts and Anim Graph. # --- Source/Engine/Core/Types/Variant.cpp | 160 +++++++++++++----- Source/Engine/Core/Types/Variant.h | 36 +++- Source/Engine/Scripting/BinaryModule.cpp | 2 + Source/Engine/Scripting/BinaryModule.h | 5 + .../Engine/Scripting/ManagedCLR/MAssembly.h | 9 + Source/Engine/Scripting/ManagedCLR/MCore.cpp | 2 + Source/Engine/Scripting/Runtime/DotNet.cpp | 1 + Source/Engine/Scripting/Scripting.cpp | 4 + Source/Engine/Serialization/Serialization.cpp | 3 + Source/Engine/Serialization/Stream.cpp | 1 + 10 files changed, 170 insertions(+), 53 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 4ab8552d3..bd2d594df 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -18,8 +18,10 @@ #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Math/Rectangle.h" #include "Engine/Core/Math/Transform.h" +#include "Engine/Scripting/BinaryModule.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Scripting/ManagedCLR/MAssembly.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ManagedCLR/MUtils.h" @@ -33,6 +35,13 @@ #endif #define AsEnum AsUint64 +// Editor can hot-reload assemblies thus cached type names may become invalid, otherwise use modules that are never unloaded and their type names are always valid +#if USE_EDITOR +#define IS_VARIANT_TYPE_NAME_STATIC(canReload) !canReload +#else +#define IS_VARIANT_TYPE_NAME_STATIC(canReload) true +#endif + namespace { const char* InBuiltTypesTypeNames[40] = @@ -88,6 +97,7 @@ static_assert((int32)VariantType::Types::MAX == ARRAY_COUNT(InBuiltTypesTypeName VariantType::VariantType(Types type, const StringView& typeName) { Type = type; + StaticName = 0; TypeName = nullptr; const int32 length = typeName.Length(); if (length) @@ -98,32 +108,41 @@ VariantType::VariantType(Types type, const StringView& typeName) } } -VariantType::VariantType(Types type, const StringAnsiView& typeName) +VariantType::VariantType(Types type, const StringAnsiView& typeName, bool staticName) { Type = type; - TypeName = nullptr; - int32 length = typeName.Length(); - if (length) + StaticName = staticName && (typeName.HasChars() && typeName[typeName.Length()] == 0); // Require string to be null-terminated (not fully safe check) + if (staticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, typeName.Get(), length); - TypeName[length] = 0; + TypeName = (char*)typeName.Get(); } + else + { + TypeName = nullptr; + int32 length = typeName.Length(); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, typeName.Get(), length); + TypeName[length] = 0; + } + } +} + +VariantType::VariantType(Types type, const ScriptingType& sType) + : VariantType(type) +{ + SetTypeName(sType); } VariantType::VariantType(Types type, const MClass* klass) { Type = type; + StaticName = false; TypeName = nullptr; #if USE_CSHARP if (klass) - { - const StringAnsiView typeName = klass->GetFullName(); - const int32 length = typeName.Length(); - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, typeName.Get(), length); - TypeName[length] = 0; - } + SetTypeName(*klass); #endif } @@ -190,9 +209,9 @@ VariantType::VariantType(const StringAnsiView& typeName) if (const auto mclass = Scripting::FindClass(typeName)) { if (mclass->IsEnum()) - new(this) VariantType(Enum, typeName); + new(this) VariantType(Enum, mclass); else - new(this) VariantType(ManagedObject, typeName); + new(this) VariantType(ManagedObject, mclass); return; } #endif @@ -204,36 +223,48 @@ VariantType::VariantType(const StringAnsiView& typeName) VariantType::VariantType(const VariantType& other) { Type = other.Type; - TypeName = nullptr; - const int32 length = StringUtils::Length(other.TypeName); - if (length) + StaticName = other.StaticName; + if (StaticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, other.TypeName, length); - TypeName[length] = 0; + TypeName = other.TypeName; + } + else + { + TypeName = nullptr; + const int32 length = StringUtils::Length(other.TypeName); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, other.TypeName, length); + TypeName[length] = 0; + } } } VariantType::VariantType(VariantType&& other) noexcept { Type = other.Type; + StaticName = other.StaticName; TypeName = other.TypeName; other.Type = Null; other.TypeName = nullptr; + other.StaticName = 0; } VariantType& VariantType::operator=(const Types& type) { Type = type; - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); TypeName = nullptr; + StaticName = 0; return *this; } VariantType& VariantType::operator=(VariantType&& other) { ASSERT(this != &other); - Swap(Type, other.Type); + Swap(Packed, other.Packed); Swap(TypeName, other.TypeName); return *this; } @@ -242,14 +273,23 @@ VariantType& VariantType::operator=(const VariantType& other) { ASSERT(this != &other); Type = other.Type; - Allocator::Free(TypeName); - TypeName = nullptr; - const int32 length = StringUtils::Length(other.TypeName); - if (length) + if (StaticName) + Allocator::Free(TypeName); + StaticName = other.StaticName; + if (StaticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, other.TypeName, length); - TypeName[length] = 0; + TypeName = other.TypeName; + } + else + { + TypeName = nullptr; + const int32 length = StringUtils::Length(other.TypeName); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, other.TypeName, length); + TypeName[length] = 0; + } } return *this; } @@ -283,24 +323,45 @@ void VariantType::SetTypeName(const StringView& typeName) { if (StringUtils::Length(TypeName) != typeName.Length()) { - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); + StaticName = 0; TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1)); TypeName[typeName.Length()] = 0; } StringUtils::ConvertUTF162ANSI(typeName.Get(), TypeName, typeName.Length()); } -void VariantType::SetTypeName(const StringAnsiView& typeName) +void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName) { - if (StringUtils::Length(TypeName) != typeName.Length()) + if (StringUtils::Length(TypeName) != typeName.Length() || StaticName != staticName) { - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); + StaticName = staticName; + if (staticName) + { + TypeName = (char*)typeName.Get(); + return; + } TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1)); TypeName[typeName.Length()] = 0; } Platform::MemoryCopy(TypeName, typeName.Get(), typeName.Length()); } +void VariantType::SetTypeName(const ScriptingType& type) +{ + SetTypeName(type.Fullname, IS_VARIANT_TYPE_NAME_STATIC(type.Module->CanReload)); +} + +void VariantType::SetTypeName(const MClass& klass) +{ +#if USE_CSHARP + SetTypeName(klass.GetFullName(), IS_VARIANT_TYPE_NAME_STATIC(klass.GetAssembly()->CanReload())); +#endif +} + const char* VariantType::GetTypeName() const { if (TypeName) @@ -322,6 +383,17 @@ VariantType VariantType::GetElementType() const return VariantType(); } +void VariantType::Inline() +{ + const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(TypeName); + if (typeHandle) + SetTypeName(typeHandle.GetType()); +#if USE_CSHARP + else if (const auto mclass = Scripting::FindClass(TypeName)) + SetTypeName(*mclass); +#endif +} + ::String VariantType::ToString() const { ::String result; @@ -632,8 +704,7 @@ Variant::Variant(ScriptingObject* v) AsObject = v; if (v) { - // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types) - Type.SetTypeName(v->GetType().Fullname); + Type.SetTypeName(v->GetType()); v->Deleted.Bind(this); } } @@ -644,9 +715,8 @@ Variant::Variant(Asset* v) AsAsset = v; if (v) { - // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types) - Type.SetTypeName(v->GetType().Fullname); v->AddReference(); + Type.SetTypeName(v->GetType()); v->OnUnloaded.Bind(this); } } @@ -3007,16 +3077,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName) switch (type.Type) { case ScriptingTypes::Script: - v.SetType(VariantType(VariantType::Object, typeName)); + v.SetType(VariantType(VariantType::Object, type)); v.AsObject = type.Script.Spawn(ScriptingObjectSpawnParams(Guid::New(), typeHandle)); if (v.AsObject) v.AsObject->Deleted.Bind(&v); break; case ScriptingTypes::Structure: - v.SetType(VariantType(VariantType::Structure, typeName)); + v.SetType(VariantType(VariantType::Structure, type)); break; case ScriptingTypes::Enum: - v.SetType(VariantType(VariantType::Enum, typeName)); + v.SetType(VariantType(VariantType::Enum, type)); v.AsEnum = 0; break; default: @@ -3030,16 +3100,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName) // Fallback to C#-only types if (mclass->IsEnum()) { - v.SetType(VariantType(VariantType::Enum, typeName)); + v.SetType(VariantType(VariantType::Enum, mclass)); v.AsEnum = 0; } else if (mclass->IsValueType()) { - v.SetType(VariantType(VariantType::Structure, typeName)); + v.SetType(VariantType(VariantType::Structure, mclass)); } else { - v.SetType(VariantType(VariantType::ManagedObject, typeName)); + v.SetType(VariantType(VariantType::ManagedObject, mclass)); MObject* instance = mclass->CreateInstance(); if (instance) { diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 4fd6ab2eb..5c057bc65 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -17,7 +17,7 @@ struct ScriptingTypeHandle; /// API_STRUCT(InBuild) struct FLAXENGINE_API VariantType { - enum Types + enum Types : uint8 { Null = 0, Void, @@ -80,10 +80,22 @@ API_STRUCT(InBuild) struct FLAXENGINE_API VariantType }; public: - /// - /// The type of the variant. - /// - Types Type; + union + { + struct + { + /// + /// The type of the variant. + /// + Types Type; + + /// + /// Internal flag used to indicate that pointer to TypeName has been linked from a static/external memory that is stable (eg. ScriptingType or MClass). Allows avoiding dynamic memory allocation. + /// + uint8 StaticName : 1; + }; + uint16 Packed; + }; /// /// The optional additional full name of the scripting type. Used for Asset, Object, Enum, Structure types to describe type precisely. @@ -94,17 +106,20 @@ public: FORCE_INLINE VariantType() { Type = Null; + StaticName = 0; TypeName = nullptr; } FORCE_INLINE explicit VariantType(Types type) { Type = type; + StaticName = 0; TypeName = nullptr; } explicit VariantType(Types type, const StringView& typeName); - explicit VariantType(Types type, const StringAnsiView& typeName); + explicit VariantType(Types type, const StringAnsiView& typeName, bool staticName = false); + explicit VariantType(Types type, const ScriptingType& sType); explicit VariantType(Types type, const MClass* klass); explicit VariantType(const StringAnsiView& typeName); VariantType(const VariantType& other); @@ -112,7 +127,8 @@ public: FORCE_INLINE ~VariantType() { - Allocator::Free(TypeName); + if (!StaticName) + Allocator::Free(TypeName); } public: @@ -130,9 +146,13 @@ public: public: void SetTypeName(const StringView& typeName); - void SetTypeName(const StringAnsiView& typeName); + void SetTypeName(const StringAnsiView& typeName, bool staticName = false); + void SetTypeName(const ScriptingType& type); + void SetTypeName(const MClass& klass); const char* GetTypeName() const; VariantType GetElementType() const; + // Drops custom type name into the name allocated by the scripting module to reduce memory allocations when referencing types. + void Inline(); ::String ToString() const; }; diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 4d26e678b..bbcd7de57 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -683,6 +683,8 @@ BinaryModule* BinaryModule::GetModule(const StringAnsiView& name) BinaryModule::BinaryModule() { + CanReload = USE_EDITOR; + // Register GetModules().Add(this); } diff --git a/Source/Engine/Scripting/BinaryModule.h b/Source/Engine/Scripting/BinaryModule.h index 70aa60fff..1da35401b 100644 --- a/Source/Engine/Scripting/BinaryModule.h +++ b/Source/Engine/Scripting/BinaryModule.h @@ -91,6 +91,11 @@ public: /// Dictionary TypeNameToTypeIndex; + /// + /// Determinates whether module can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some modules such as engine and class library modules are static. + /// + bool CanReload; + public: /// diff --git a/Source/Engine/Scripting/ManagedCLR/MAssembly.h b/Source/Engine/Scripting/ManagedCLR/MAssembly.h index 6c0aa9579..0a785c06a 100644 --- a/Source/Engine/Scripting/ManagedCLR/MAssembly.h +++ b/Source/Engine/Scripting/ManagedCLR/MAssembly.h @@ -34,6 +34,7 @@ private: int32 _isLoaded : 1; int32 _isLoading : 1; + int32 _canReload : 1; mutable int32 _hasCachedClasses : 1; mutable ClassesDictionary _classes; @@ -125,6 +126,14 @@ public: return _isLoaded != 0; } + /// + /// Returns true if assembly can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some assemblies such as engine and class library modules are static. + /// + FORCE_INLINE bool CanReload() const + { + return USE_EDITOR && _canReload; + } + /// /// Gets the assembly name. /// diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.cpp b/Source/Engine/Scripting/ManagedCLR/MCore.cpp index 350cc39d2..6fa499002 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MCore.cpp @@ -45,6 +45,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name) : _domain(domain) , _isLoaded(false) , _isLoading(false) + , _canReload(true) , _hasCachedClasses(false) , _reloadCount(0) , _name(name) @@ -59,6 +60,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name, const StringAn , _domain(domain) , _isLoaded(false) , _isLoading(false) + , _canReload(true) , _hasCachedClasses(false) , _reloadCount(0) , _name(name) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 1c8c2bcdd..4be0ce1a1 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -874,6 +874,7 @@ bool MAssembly::LoadCorlib() return true; } _hasCachedClasses = false; + _canReload = false; CachedAssemblyHandles.Add(_handle, this); // End diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index 4e17bb80a..aa7e26674 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -502,6 +502,7 @@ bool Scripting::LoadBinaryModules(const String& path, const String& projectFolde // C# if (managedPath.HasChars() && !((ManagedBinaryModule*)module)->Assembly->IsLoaded()) { + (((ManagedBinaryModule*)module)->Assembly)->_canReload = module->CanReload; if (((ManagedBinaryModule*)module)->Assembly->Load(managedPath, nativePath)) { LOG(Error, "Failed to load C# assembly '{0}' for binary module {1}.", managedPath, name); @@ -528,6 +529,7 @@ bool Scripting::Load() #if USE_CSHARP // Load C# core assembly ManagedBinaryModule* corlib = GetBinaryModuleCorlib(); + corlib->CanReload = false; if (corlib->Assembly->LoadCorlib()) { LOG(Error, "Failed to load corlib C# assembly."); @@ -581,6 +583,8 @@ bool Scripting::Load() LOG(Error, "Failed to load FlaxEngine C# assembly."); return true; } + flaxEngineModule->CanReload = false; + flaxEngineModule->Assembly->_canReload = false; onEngineLoaded(flaxEngineModule->Assembly); // Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types) diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index a3dfc6ffa..1eb6b0181 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -78,7 +78,10 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian v.Type = VariantType::Null; const auto mTypeName = SERIALIZE_FIND_MEMBER(stream, "TypeName"); if (mTypeName != stream.MemberEnd() && mTypeName->value.IsString()) + { v.SetTypeName(StringAnsiView(mTypeName->value.GetStringAnsiView())); + v.Inline(); + } } else { diff --git a/Source/Engine/Serialization/Stream.cpp b/Source/Engine/Serialization/Stream.cpp index f95e9ef9b..4c9b94042 100644 --- a/Source/Engine/Serialization/Stream.cpp +++ b/Source/Engine/Serialization/Stream.cpp @@ -255,6 +255,7 @@ void ReadStream::Read(VariantType& data) ptr++; } *ptr = 0; + data.Inline(); } else if (typeNameLength > 0) {