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.

#
This commit is contained in:
Wojtek Figat
2026-02-09 18:01:47 +01:00
parent bd300651ec
commit 7b7a92758f
10 changed files with 170 additions and 53 deletions

View File

@@ -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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<char*>(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<Variant, &Variant::OnObjectDeleted>(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<Variant, &Variant::OnAssetUnloaded>(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<Variant, &Variant::OnObjectDeleted>(&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)
{

View File

@@ -17,7 +17,7 @@ struct ScriptingTypeHandle;
/// </summary>
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:
/// <summary>
/// The type of the variant.
/// </summary>
Types Type;
union
{
struct
{
/// <summary>
/// The type of the variant.
/// </summary>
Types Type;
/// <summary>
/// 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.
/// </summary>
uint8 StaticName : 1;
};
uint16 Packed;
};
/// <summary>
/// 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;
};

View File

@@ -683,6 +683,8 @@ BinaryModule* BinaryModule::GetModule(const StringAnsiView& name)
BinaryModule::BinaryModule()
{
CanReload = USE_EDITOR;
// Register
GetModules().Add(this);
}

View File

@@ -91,6 +91,11 @@ public:
/// </summary>
Dictionary<StringAnsi, int32> TypeNameToTypeIndex;
/// <summary>
/// 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.
/// </summary>
bool CanReload;
public:
/// <summary>

View File

@@ -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;
}
/// <summary>
/// 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.
/// </summary>
FORCE_INLINE bool CanReload() const
{
return USE_EDITOR && _canReload;
}
/// <summary>
/// Gets the assembly name.
/// </summary>

View File

@@ -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)

View File

@@ -874,6 +874,7 @@ bool MAssembly::LoadCorlib()
return true;
}
_hasCachedClasses = false;
_canReload = false;
CachedAssemblyHandles.Add(_handle, this);
// End

View File

@@ -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)

View File

@@ -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
{

View File

@@ -255,6 +255,7 @@ void ReadStream::Read(VariantType& data)
ptr++;
}
*ptr = 0;
data.Inline();
}
else if (typeNameLength > 0)
{