Files
UnrealEngine/Engine/Plugins/Experimental/PlainPropsUObject/Source/Private/PlainPropsUObjectBindings.cpp
2025-05-18 13:04:45 +08:00

4490 lines
146 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PlainPropsUObjectRuntime.h"
#include "PlainPropsUeCoreBindings.h"
#include "PlainPropsBuildSchema.h"
#include "PlainPropsDiff.h"
#include "PlainPropsParse.h"
#include "PlainPropsPrint.h"
#include "PlainPropsRead.h"
#include "PlainPropsVisualize.h"
#include "PlainPropsWrite.h"
#include "Algo/Compare.h"
#include "Algo/Find.h"
#include "Containers/PagedArray.h"
#include "HAL/FileManager.h"
#include "Hash/xxhash.h"
#include "Logging/StructuredLog.h"
#include "Math/PreciseFP.h"
#include "Misc/AsciiSet.h"
#include "Misc/CommandLine.h"
#include "Misc/CoreDelegates.h"
#include "Misc/DefinePrivateMemberPtr.h"
#include "Misc/Paths.h"
#include "Serialization/Archive.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "StructUtils/UserDefinedStruct.h"
#include "Templates/UniquePtr.h"
#include "UObject/AnsiStrProperty.h"
#include "UObject/Class.h"
#include "UObject/EnumProperty.h"
#include "UObject/FieldPathProperty.h"
#include "UObject/Object.h"
#include "UObject/PropertyOptional.h"
#include "UObject/StrProperty.h"
#include "UObject/TextProperty.h"
#include "UObject/Utf8StrProperty.h"
#include "UObject/UnrealType.h"
#include "UObject/UObjectIterator.h"
#include "UObject/VerseValueProperty.h"
#include "UObject/VerseStringProperty.h"
using FUnicastScriptDelegate = TScriptDelegate<FNotThreadSafeNotCheckedDelegateMode>;
using FMulticastInvocationList = TArray<FUnicastScriptDelegate>;
using FMulticastInvocationView = TConstArrayView<FUnicastScriptDelegate>;
using FDelegateBase = TDelegateAccessHandlerBase<FNotThreadSafeDelegateMode>;
// Temp hacks. Long-term either add FProperty getters for ctor/dtor/hash function pointers
// and delegate APIs for non-intrusive serialization or integrate PlainProps into Core/CoreUObject
UE_DEFINE_PRIVATE_MEMBER_PTR(void(void*) const, GInitPropertyValue, FProperty, InitializeValueInternal);
UE_DEFINE_PRIVATE_MEMBER_PTR(void(void*) const, GDestroyPropertyValue, FProperty, DestroyValueInternal);
UE_DEFINE_PRIVATE_MEMBER_PTR(uint32(const void*) const, GHashPropertyValue, FProperty, GetValueTypeHashInternal);
UE_DEFINE_PRIVATE_MEMBER_PTR(TArray<FName>, GFieldPathPath, FFieldPath, Path);
UE_DEFINE_PRIVATE_MEMBER_PTR(TWeakObjectPtr<UStruct>, GFieldPathOwner, FFieldPath, ResolvedOwner);
UE_DEFINE_PRIVATE_MEMBER_PTR(FWeakObjectPtr, GDelegateObject, FScriptDelegate, Object);
UE_DEFINE_PRIVATE_MEMBER_PTR(FName, GDelegateFunctionName, FScriptDelegate, FunctionName);
UE_DEFINE_PRIVATE_MEMBER_PTR(FWeakObjectPtr, GUnicastDelegateObject, FUnicastScriptDelegate, Object);
UE_DEFINE_PRIVATE_MEMBER_PTR(FName, GUnicastDelegateFunctionName, FUnicastScriptDelegate, FunctionName);
UE_DEFINE_PRIVATE_MEMBER_PTR(FMulticastInvocationList, GMulticastDelegateInvocationList, FMulticastScriptDelegate, InvocationList);
UE_DEFINE_PRIVATE_MEMBER_PTR(bool, GSparseDelegateIsBound, FSparseDelegate, bIsBound);
#if UE_DETECT_DELEGATES_RACE_CONDITIONS && 0
UE_DEFINE_PRIVATE_MEMBER_PTR(FMRSWRecursiveAccessDetector, GDelegateAccessDetector, FDelegateBase, AccessDetector);
struct FDelegateAccess : public FDelegateBase
{
struct FReadScope : public FDelegateBase::FReadAccessScope
{
explicit FReadScope(const FDelegateBase& In) : FDelegateBase::FReadAccessScope(In.*GDelegateAccessDetector) {}
};
struct FWriteScope : public FDelegateBase::FWriteAccessScope
{
explicit FWriteScope(FDelegateBase& In) : FDelegateBase::FWriteAccessScope(In.*GDelegateAccessDetector) {}
};
};
#else
struct FDelegateAccess
{
struct FReadScope { FReadScope(const FDelegateBase& In) {} };
using FWriteScope = FReadScope;
};
#endif // UE_DETECT_DELEGATES_RACE_CONDITIONS
DEFINE_LOG_CATEGORY_STATIC(LogPlainPropsUObject, Log, All);
namespace PlainProps::UE
{
static constexpr ERangeSizeType DefaultRangeMax = RangeSizeOf(FDefaultAllocator::SizeType{});
inline FMemberSpec DefaultRangeOf(FMemberSpec Item) { return FMemberSpec(DefaultRangeMax, Item); }
////////////////////////////////////////////////////////////////////////////////////////////////
struct FDefaultStruct
{
UScriptStruct::ICppStructOps& Ops;
alignas(16) uint8 Instance[0];
};
static FDefaultStruct* NewDefaultStruct(UScriptStruct::ICppStructOps& Ops)
{
check(Ops.GetAlignment() <= 16);
uint32 Size = sizeof(FDefaultStruct) + Ops.GetSize();
FDefaultStruct Header = {Ops};
FDefaultStruct* Out = new (FMemory::MallocZeroed(Size)) FDefaultStruct(Header);
Ops.Construct(Out->Instance);
return Out;
}
inline void DeleteDefaultStruct(uint8* Instance)
{
FDefaultStruct* Struct = reinterpret_cast<FDefaultStruct*>(Instance - offsetof(FDefaultStruct, Instance));
if (Struct->Ops.HasDestructor())
{
Struct->Ops.Destruct(Instance);
}
FMemory::Free(Struct);
}
static constexpr uint64 DefaultInstanceStaticMask = 1;
inline FDefaultInstance MakeStaticInstance(const void* Static) { return { reinterpret_cast<uint64>(Static) | DefaultInstanceStaticMask }; }
inline FDefaultInstance MakeDefaultInstance(FDefaultStruct* Default) { return { reinterpret_cast<uint64>(Default->Instance) }; }
inline uint8* GetInstance(FDefaultInstance Instance) { return reinterpret_cast<uint8*>(Instance.Ptr & ~DefaultInstanceStaticMask); }
inline void DeleteInstance(FDefaultInstance Instance)
{
if (!(Instance.Ptr & DefaultInstanceStaticMask))
{
DeleteDefaultStruct(reinterpret_cast<uint8*>(Instance.Ptr));
}
}
static void ReserveZeroes(/* in-out */ FMutableMemoryView& Zeroes, SIZE_T Size, uint32 Alignment)
{
Size += FMath::Max<int32>(0, Alignment - 16);
Size = Align(Size, 4096);
if (Zeroes.GetSize() < Size)
{
FMemory::Free(Zeroes.GetData());
Zeroes = FMutableMemoryView(FMemory::MallocZeroed(Size, 16), Size);
}
}
FDefaultStructs::~FDefaultStructs()
{
for (TPair<FBindId, FDefaultInstance> Instance : Instances)
{
DeleteInstance(Instance.Value);
}
}
inline bool Flip(FBitReference Bit)
{
Bit = !Bit;
return Bit;
}
void FDefaultStructs::ReserveFlags(uint32 Idx)
{
if (Idx >= static_cast<uint32>(Instanced.Num()))
{
Instanced.SetNum(FMath::RoundUpToPowerOfTwo64(Idx + 1), false);
#if DO_CHECK
Bound.SetNum(Instanced.Num(), false);
#endif
}
}
void FDefaultStructs::Bind(FBindId Id, const UScriptStruct* Struct)
{
const EStructFlags Flags = Struct->StructFlags;
const SIZE_T Size = Struct->GetStructureSize();
const uint32 Alignment = Struct->GetMinAlignment();
UScriptStruct::ICppStructOps* Ops = Struct->GetCppStructOps();
ReserveFlags(Id.Idx);
#if DO_CHECK
checkf(Flip(Bound[Id.Idx]), TEXT("'%s' already bound"), *GUE.Debug.Print(Id));
#endif
if (const UUserDefinedStruct* UserStruct = Cast<UUserDefinedStruct>(Struct))
{
const void* DefaultInstance = UserStruct->GetDefaultInstance();
check(DefaultInstance);
if (FMemory::MemIsZero(DefaultInstance, Size))
{
ReserveZeroes(Zeroes, Size, Alignment);
}
else
{
Instanced[Id.Idx] = true;
Instances.Emplace(Id, MakeStaticInstance(DefaultInstance));
}
}
else if (!!(Flags & STRUCT_ZeroConstructor) || Ops == nullptr)
{
ReserveZeroes(Zeroes, Size, Alignment);
}
else
{
check(Ops->GetSize() == Size);
FDefaultStruct* Default = NewDefaultStruct(*Ops);
if (FMemory::MemIsZero(Default->Instance, Size))
{
DeleteDefaultStruct(Default->Instance);
ReserveZeroes(Zeroes, Size, Alignment);
}
else
{
Instanced[Id.Idx] = true;
Instances.Add(Id, MakeDefaultInstance(Default));
}
}
}
void FDefaultStructs::BindZeroes(FBindId Id, SIZE_T Size, uint32 Alignment)
{
ReserveFlags(Id.Idx);
#if DO_CHECK
checkf(Flip(Bound[Id.Idx]), TEXT("'%s' already bound"), *GUE.Debug.Print(Id));
#endif
ReserveZeroes(Zeroes, Size, Alignment);
}
void FDefaultStructs::BindStatic(FBindId Id, const void* Struct)
{
ReserveFlags(Id.Idx);
#if DO_CHECK
checkf(Flip(Bound[Id.Idx]), TEXT("'%s' already bound"), *GUE.Debug.Print(Id));
#endif
check(!Instanced[Id.Idx]);
check(GetInstance(MakeStaticInstance(Struct)) == Struct);
Instanced[Id.Idx] = true;
Instances.Add(Id, MakeStaticInstance(Struct));
}
void FDefaultStructs::Drop(FBindId Id)
{
#if DO_CHECK
checkf(!Flip(Bound[Id.Idx]), TEXT("'%s' isn't bound"), *GUE.Debug.Print(Id));
#endif
if (Instanced[Id.Idx])
{
Instanced[Id.Idx] = false;
DeleteInstance(Instances.FindAndRemoveChecked(Id));
}
}
const void* FDefaultStructs::Get(FBindId Id)
{
#if DO_CHECK
checkf(Bound[Id.Idx], TEXT("'%s' lack default"), *GUE.Debug.Print(Id));
#endif
return Instanced[Id.Idx] ? GetInstance(Instances.FindChecked(Id)) : Zeroes.GetData();
}
////////////////////////////////////////////////////////////////////////////////////////////////
FCommonScopeIds::FCommonScopeIds(TIdIndexer<FSensitiveName>& Names)
: Core( Names.MakeScope("/Script/Core"))
, CoreUObject( Names.MakeScope("/Script/CoreUObject"))
{}
FCommonTypenameIds::FCommonTypenameIds(TIdIndexer<FSensitiveName>& Names)
: Optional( Names.NameType("Optional"))
, Map( Names.NameType("Map"))
, Set( Names.NameType("Set"))
, Pair( Names.NameType("Pair"))
, LeafArray( Names.NameType("LeafArray"))
, TrivialArray( Names.NameType("TrivialArray"))
, NonTrivialArray( Names.NameType("NonTrivialArray"))
, StaticArray( Names.NameType("StaticArray"))
, TrivialOptional( Names.NameType("TrivialOptional"))
, IntrusiveOptional( Names.NameType("IntrusiveOptional"))
, NonIntrusiveOptional( Names.NameType("NonIntrusiveOptional"))
, String( Names.NameType("String"))
, Utf8String( Names.NameType("Utf8String"))
, AnsiString( Names.NameType("AnsiString"))
, VerseString( Names.NameType("VerseString"))
{}
FCommonStructIds::FCommonStructIds(const FCommonScopeIds& Scopes, TIdIndexer<FSensitiveName>& Names)
: Name( Names.IndexStruct({Scopes.Core, Names.MakeTypename("Name")}))
, Text( Names.IndexStruct({Scopes.Core, Names.MakeTypename("Text")}))
, Guid( Names.IndexStruct({Scopes.Core, Names.MakeTypename("Guid")}))
, FieldPath( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("FieldPath")}))
, SoftObjectPath( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("SoftObjectPath")}))
, ClassPtr( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("ClassPtr")}))
, ObjectPtr( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("ObjectPtr")}))
, WeakObjectPtr( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("WeakObjectPtr")}))
, LazyObjectPtr( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("LazyObjectPtr")}))
, SoftObjectPtr( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("SoftObjectPtr")}))
, ScriptInterface( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("ScriptInterface")}))
, Delegate( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("Delegate")}))
, MulticastDelegate( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("MulticastDelegate")}))
, MulticastInlineDelegate( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("MulticastInlineDelegate")}))
, MulticastSparseDelegate( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("MulticastSparseDelegate")}))
, VerseFunction( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("VerseFunction")}))
, DynamicallyTypedValue( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("DynamicallyTypedValue")}))
, ReferencePropertyValue( Names.IndexStruct({Scopes.CoreUObject, Names.MakeTypename("ReferencePropertyValue")}))
{}
FCommonMemberIds::FCommonMemberIds(TIdIndexer<FSensitiveName>& Names)
: Key( Names.NameMember("Key"))
, Value( Names.NameMember("Value"))
, Assign( Names.NameMember("Assign"))
, Remove( Names.NameMember("Remove"))
, Insert( Names.NameMember("Insert"))
, Id( Names.NameMember("Id"))
, Object( Names.NameMember("Object"))
, Function( Names.NameMember("Function"))
, Invocations( Names.NameMember("Invocations"))
, Path( Names.NameMember("Path"))
, Owner( Names.NameMember("Owner"))
{}
////////////////////////////////////////////////////////////////////////////////////////////////
FGlobals::FGlobals()
: Enums(FDebugIds(Names))
, Schemas(FDebugIds(Names))
, Customs(FDebugIds(Names))
, Scopes(Names)
, Structs(Scopes, Names)
, Typenames(Names)
, Members(Names)
, Numerals(Names)
, Debug(Names)
{}
FGlobals GUE;
////////////////////////////////////////////////////////////////////////////////////////////////
static constexpr uint64 LeafMask = CASTCLASS_FNumericProperty | CASTCLASS_FEnumProperty| CASTCLASS_FBoolProperty;
static constexpr uint64 IntSMask = CASTCLASS_FInt8Property | CASTCLASS_FInt16Property | CASTCLASS_FIntProperty | CASTCLASS_FInt64Property;
static constexpr uint64 IntUMask = CASTCLASS_FByteProperty | CASTCLASS_FUInt16Property | CASTCLASS_FUInt32Property | CASTCLASS_FUInt64Property;
static constexpr uint64 ContainerMask = CASTCLASS_FArrayProperty | CASTCLASS_FSetProperty | CASTCLASS_FMapProperty | CASTCLASS_FOptionalProperty;
static constexpr uint64 StringMask = CASTCLASS_FStrProperty | CASTCLASS_FUtf8StrProperty | CASTCLASS_FAnsiStrProperty | CASTCLASS_FVerseStringProperty;
static constexpr uint64 CommonStructMask = CASTCLASS_FNameProperty | CASTCLASS_FTextProperty | CASTCLASS_FFieldPathProperty | CASTCLASS_FClassProperty |
CASTCLASS_FObjectProperty | CASTCLASS_FWeakObjectProperty | CASTCLASS_FSoftObjectProperty | CASTCLASS_FLazyObjectProperty |
CASTCLASS_FDelegateProperty | CASTCLASS_FMulticastInlineDelegateProperty;
static constexpr uint64 MiscMask = CASTCLASS_FMulticastSparseDelegateProperty | CASTCLASS_FInterfaceProperty;
static FBindId FlagsToCommonBindId(uint64 MaskedCastFlags)
{
switch (MaskedCastFlags)
{
case CASTCLASS_FNameProperty: return GUE.Structs.Name;
case CASTCLASS_FClassProperty | CASTCLASS_FObjectProperty: return GUE.Structs.ClassPtr;
case CASTCLASS_FObjectProperty: return GUE.Structs.ObjectPtr;
case CASTCLASS_FWeakObjectProperty: return GUE.Structs.WeakObjectPtr;
case CASTCLASS_FSoftObjectProperty: return GUE.Structs.SoftObjectPtr;
case CASTCLASS_FLazyObjectProperty: return GUE.Structs.LazyObjectPtr;
case CASTCLASS_FDelegateProperty: return GUE.Structs.Delegate;
case CASTCLASS_FMulticastInlineDelegateProperty: return GUE.Structs.MulticastInlineDelegate;
case CASTCLASS_FTextProperty: return GUE.Structs.Text;
case CASTCLASS_FFieldPathProperty: return GUE.Structs.FieldPath;
default: break; // error
}
check(MaskedCastFlags); // @pre violated
check((MaskedCastFlags & CommonStructMask) == MaskedCastFlags); // @pre violated
checkf(FMath::CountBits(MaskedCastFlags) == 1, TEXT("Masked CASTCLASS flags %llx match more than one common property type"), MaskedCastFlags);
check(false); // Mismatch between this function and CommonStructMask
return {};
}
template<uint64 Mask>
static bool HasAny(uint64 Flags)
{
return (Mask & Flags) != 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////
static FType IndexType(const UField* Field)
{
check(Field);
FTypenameId Name = GUE.Names.MakeTypename(Field->GetFName());
// This wouldn't be needed in an intrusive or cached solution
TArray<FFlatScopeId, TInlineAllocator<64>> ReversedOuters;
for (const UObject* Outer = Field->GetOuter(); Outer; Outer = Outer->GetOuter())
{
ReversedOuters.Add(GUE.Names.NameScope(Outer->GetFName()));
}
return { GUE.Names.NestReversedScopes(ReversedOuters), Name };
}
static EMemberPresence GetOccupancy(const UStruct* Struct)
{
if (Struct->HasAnyCastFlags(CASTCLASS_UScriptStruct))
{
EStructFlags Flags = static_cast<const UScriptStruct*>(Struct)->StructFlags;
return (Flags & (STRUCT_Immutable | STRUCT_Atomic)) ? EMemberPresence::RequireAll : EMemberPresence::AllowSparse;
}
return EMemberPresence::AllowSparse;
}
////////////////////////////////////////////////////////////////////////////////////////////////
static FTypedRange SaveNames(TConstArrayView<FName> Names, const FSaveContext& Ctx)
{
FBindId Id = GUE.Structs.Name;
FStructRangeSaver Out(Ctx.Scratch, Names.Num());
for (FName Name : Names)
{
Out.AddItem(SaveStruct(&Name, Id, Ctx));
}
return Out.Finalize(MakeStructRangeSchema(DefaultRangeMax, Id));
}
static void LoadNames(TArray<FName>& Dst, FStructRangeLoadView Src)
{
Dst.SetNumUninitialized(static_cast<int32>(Src.Num()));
FName* DstIt = Dst.GetData();
for (FStructLoadView Name : Src)
{
LoadStruct(DstIt++, Name);
}
}
FFieldPathBinding::FFieldPathBinding(TPropertySpecifier<2>& Spec)
: MemberIds{GUE.Members.Path, GUE.Members.Owner}
{
Spec.Members[0] = DefaultRangeOf(FDeclId(GUE.Structs.Name));
Spec.Members[1] = FDeclId(GUE.Structs.WeakObjectPtr);
}
void FFieldPathBinding::Save(FMemberBuilder& Dst, const FFieldPath& Src, const FFieldPath*, const FSaveContext& Ctx) const
{
Dst.AddRange(GUE.Members.Path, SaveNames(Src.*GFieldPathPath, Ctx));
Dst.AddStruct(GUE.Members.Owner, GUE.Structs.WeakObjectPtr, SaveStruct(&(Src.*GFieldPathOwner), GUE.Structs.WeakObjectPtr, Ctx));
}
void FFieldPathBinding::Load(FFieldPath& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
FMemberLoader Members(Src);
Dst.Reset(); // ClearCachedField() more optimal
LoadNames(Dst.*GFieldPathPath, Members.GrabRange().AsStructs());
LoadStruct(&(Dst.*GFieldPathOwner), Members.GrabStruct());
}
bool FFieldPathBinding::Diff(const FFieldPath& A, const FFieldPath& B, const FBindContext&)
{
return A != B;
}
////////////////////////////////////////////////////////////////////////////////////////////////
FDelegateBinding::FDelegateBinding(TPropertySpecifier<2>& Out)
: MemberIds{GUE.Members.Object, GUE.Members.Function}
{
Out.SetMembers(GUE.Structs.WeakObjectPtr, GUE.Structs.Name);
}
void FDelegateBinding::Save(FMemberBuilder& Dst, const FScriptDelegate& Src, const FScriptDelegate* Default, const FSaveContext& Ctx) const
{
FDelegateAccess::FReadScope Scope(Src);
if (FName Function = Src.*GDelegateFunctionName; Function != FName())
{
Dst.AddStruct(GUE.Members.Object, GUE.Structs.WeakObjectPtr, SaveStruct(&(Src.*GDelegateObject), GUE.Structs.WeakObjectPtr, Ctx));
Dst.AddStruct(GUE.Members.Function, GUE.Structs.Name, SaveStruct(&Function, GUE.Structs.Name, Ctx));
}
}
void FDelegateBinding::Load(FScriptDelegate& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
FMemberLoader Members(Src);
if (Members.HasMore())
{
FDelegateAccess::FWriteScope Scope(Dst);
LoadStruct(&(Dst.*GDelegateObject), Members.GrabStruct());
LoadStruct(&(Dst.*GDelegateFunctionName), Members.GrabStruct());
}
else
{
Dst.Clear();
}
}
bool FDelegateBinding::Diff(const FScriptDelegate& A, const FScriptDelegate& B, const FBindContext&)
{
return A != B;
}
////////////////////////////////////////////////////////////////////////////////////////////////
static void SaveUnicastDelegate(FMemberBuilder& Dst, const FUnicastScriptDelegate& Src, const FSaveContext& Ctx)
{
Dst.AddStruct(GUE.Members.Object, GUE.Structs.WeakObjectPtr, SaveStruct(&(Src.*GUnicastDelegateObject), GUE.Structs.WeakObjectPtr, Ctx));
Dst.AddStruct(GUE.Members.Function, GUE.Structs.Name, SaveStruct(&(Src.*GUnicastDelegateFunctionName), GUE.Structs.Name, Ctx));
}
static void LoadUnicastDelegate(FUnicastScriptDelegate& Dst, FStructLoadView Src)
{
FMemberLoader Members(Src);
LoadStruct(&(Dst.*GUnicastDelegateObject), Members.GrabStruct());
LoadStruct(&(Dst.*GUnicastDelegateFunctionName), Members.GrabStruct());
}
static FTypedRange SaveInvocations(const FMulticastInvocationList& In, const FSaveContext& Ctx)
{
FBindId ItemId = GUE.Structs.Delegate;
FMemberSchema Schema = MakeStructRangeSchema(DefaultRangeMax, ItemId);
if (int32 NumTotal = In.Num())
{
TBitArray<> Keep;
Keep.Reserve(NumTotal);
for (const FUnicastScriptDelegate& Invocation : In)
{
Keep.Add(!Invocation.IsCompactable());
}
if (int32 NumKept = Keep.CountSetBits())
{
const FStructDeclaration& ItemDecl = Ctx.GetDeclaration(ItemId);
const FUnicastScriptDelegate* Src = In.GetData();
FStructRangeSaver Dst(Ctx.Scratch, static_cast<uint64>(NumKept));
FMemberBuilder Tmp;
for (int32 Idx = 0; Idx < NumTotal; ++Idx)
{
if (Keep[Idx])
{
SaveUnicastDelegate(/* out */ Tmp, Src[Idx], Ctx);
Dst.AddItem(Tmp.BuildAndReset(Ctx.Scratch, ItemDecl, GUE.Debug));
}
}
return Dst.Finalize(Schema);
}
}
return {Schema, nullptr};
}
static FStructDeclarationPtr DeclareMulticastDelegate()
{
return Declare({GUE.Structs.MulticastDelegate, NoId, 0, EMemberPresence::RequireAll, {GUE.Members.Invocations}, {DefaultRangeOf(FDeclId(GUE.Structs.Delegate))}});
}
static void SaveMulticastDelegate(FMemberBuilder& Dst, const FMulticastScriptDelegate& Src, const FSaveContext& Ctx)
{
FDelegateAccess::FReadScope Scope(Src);
Dst.AddRange(GUE.Members.Invocations, SaveInvocations(Src.*GMulticastDelegateInvocationList, Ctx));
}
static void SaveEmptyMulticastDelegate(FMemberBuilder& Dst)
{
Dst.AddRange(GUE.Members.Invocations, { MakeStructRangeSchema(DefaultRangeMax, GUE.Structs.Delegate), nullptr });
}
static void LoadInvocations(FMulticastInvocationList& Dst, FStructRangeLoadView Src)
{
Dst.Reset(static_cast<int32>(Src.Num()));
for (FStructLoadView Invocation : Src)
{
LoadUnicastDelegate(Dst.AddDefaulted_GetRef(), Invocation);
}
}
static void LoadMulticastDelegate(FMulticastScriptDelegate& Dst, FMemberLoader& Src)
{
FDelegateAccess::FWriteScope Scope(Dst);
LoadInvocations(Dst.*GMulticastDelegateInvocationList, Src.GrabRange().AsStructs());
}
inline bool DiffInvocations(TConstArrayView<FUnicastScriptDelegate> A, TConstArrayView<FUnicastScriptDelegate> B)
{
if (A.Num() + B.Num() == 0)
{
return false;
}
const FUnicastScriptDelegate* EndA = A.GetData() + A.Num();
const FUnicastScriptDelegate* EndB = B.GetData() + B.Num();
for (const FUnicastScriptDelegate* ItA = A.GetData(), *ItB = B.GetData(); true; ++ItA, ++ItB)
{
for (; ItA != EndA && ItA->IsCompactable(); ++ItA) {}
for (; ItB != EndB && ItB->IsCompactable(); ++ItB) {}
if (ItA == EndA || ItB == EndB)
{
return ItA != EndA || ItB != EndB;
}
else if (*ItA != *ItB)
{
return true;
}
}
}
static bool DiffMulticastDelegate(const FMulticastScriptDelegate& A, const FMulticastScriptDelegate& B)
{
return DiffInvocations(A.*GMulticastDelegateInvocationList, B.*GMulticastDelegateInvocationList);
}
FMulticastInlineDelegateBinding::FMulticastInlineDelegateBinding(TPropertySpecifier<1>& Spec)
: MemberIds{GUE.Members.Invocations}
{
Spec.Members[0] = DefaultRangeOf(FDeclId(GUE.Structs.Delegate));
}
void FMulticastInlineDelegateBinding::Save(FMemberBuilder& Dst, const FMulticastScriptDelegate& Src, const FMulticastScriptDelegate* Default, const FSaveContext& Ctx) const
{
SaveMulticastDelegate(Dst, Src, Ctx);
}
void FMulticastInlineDelegateBinding::Load(FMulticastScriptDelegate& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
check(Method == ECustomLoadMethod::Assign);
FMemberLoader Members(Src);
LoadMulticastDelegate(Dst, Members);
}
bool FMulticastInlineDelegateBinding::Diff(const FMulticastScriptDelegate& A, const FMulticastScriptDelegate& B, const FBindContext&)
{
return DiffMulticastDelegate(A, B);
}
////////////////////////////////////////////////////////////////////////////////////////////////
struct FMulticastSparseDelegateBinding final : ICustomBinding
{
explicit FMulticastSparseDelegateBinding(const USparseDelegateFunction* SignatureFunction)
: OwningClassName(SignatureFunction->OwningClassName)
, DelegateName(SignatureFunction->DelegateName)
{}
const FName OwningClassName;
const FName DelegateName;
virtual void SaveCustom(FMemberBuilder& Dst, const void* Src, const void* Default, const FSaveContext& Ctx) override
{
if (!Default || DiffCustom(Src, Default, Ctx))
{
Save(Dst, *static_cast<const FSparseDelegate*>(Src), Ctx);
}
}
virtual void LoadCustom(void* Dst, FStructLoadView Src, ECustomLoadMethod Method) const override
{
check(Method == ECustomLoadMethod::Assign);
Load(*static_cast<FSparseDelegate*>(Dst), Src);
}
virtual bool DiffCustom(const void* A, const void* B, const FBindContext&) const override
{
return Diff(*static_cast<const FSparseDelegate*>(A), *static_cast<const FSparseDelegate*>(B));
}
const FMulticastScriptDelegate* GetMulticastDelegate(const FSparseDelegate& Sparse) const
{
if (Sparse.IsBound())
{
const UObject* Owner = FSparseDelegateStorage::ResolveSparseOwner(Sparse, OwningClassName, DelegateName);
return FSparseDelegateStorage::GetMulticastDelegate(Owner, DelegateName);
}
return nullptr;
}
void Save(FMemberBuilder& Dst, const FSparseDelegate& Src, const FSaveContext& Ctx) const
{
if (const FMulticastScriptDelegate* Delegate = GetMulticastDelegate(Src))
{
SaveMulticastDelegate(Dst, *Delegate, Ctx);
}
else
{
SaveEmptyMulticastDelegate(Dst);
}
}
void Load(FSparseDelegate& Dst, FStructLoadView Src) const
{
if (Dst.IsBound())
{
UObject* Owner = FSparseDelegateStorage::ResolveSparseOwner(Dst, OwningClassName, DelegateName);
FSparseDelegateStorage::Clear(Owner, DelegateName);
Dst.*GSparseDelegateIsBound = false;
}
FMemberLoader Members(Src);
if (Members.HasMore())
{
UObject* Owner = FSparseDelegateStorage::ResolveSparseOwner(Dst, OwningClassName, DelegateName);
FMulticastScriptDelegate Tmp;
LoadMulticastDelegate(Tmp, Members);
FSparseDelegateStorage::SetMulticastDelegate(Owner, DelegateName, MoveTemp(Tmp));
Dst.*GSparseDelegateIsBound = true;
}
}
bool Diff(const FSparseDelegate& SparseA, const FSparseDelegate& SparseB) const
{
const FMulticastScriptDelegate* A = GetMulticastDelegate(SparseA);
const FMulticastScriptDelegate* B = GetMulticastDelegate(SparseB);
if (A && B)
{
return DiffMulticastDelegate(*A, *B);
}
return !!A != !!B;
}
};
static FBindId BindSparseDelegate(FBindId Owner, FMulticastSparseDelegateProperty* Property)
{
// Todo: Ownership / memory leak
ICustomBinding* Leak = new FMulticastSparseDelegateBinding(CastChecked<USparseDelegateFunction>(Property->SignatureFunction));
FType MulticastSparseDelegate = GUE.Names.Resolve(GUE.Structs.MulticastSparseDelegate);
FType OwnerParam = GUE.Names.Resolve(Owner);
FType PropertyParam = { GUE.Scopes.CoreUObject, FTypenameId(GUE.Names.NameType(Property->GetFName())) };
FType UniqueBindName = GUE.Names.MakeParametricType(MulticastSparseDelegate, {OwnerParam, PropertyParam});
FBindId Id = GUE.Names.IndexBindId(UniqueBindName);
static FStructDeclarationPtr Declaration = DeclareMulticastDelegate();
GUE.Customs.BindStruct(Id, *Leak, FStructDeclarationPtr(Declaration), {});
return Id;
}
////////////////////////////////////////////////////////////////////////////////////////////////
FVerseFunctionBinding::FVerseFunctionBinding(TPropertySpecifier<1>& Spec)
: MemberIds{GUE.Members.Value}
{
Spec.Members[0] = SpecDynamicStruct;
}
void FVerseFunctionBinding::Save(FMemberBuilder& Dst, const FVerseFunction& Src, const FVerseFunction* Default, const FSaveContext& Ctx) const
{
unimplemented();
}
void FVerseFunctionBinding::Load(FVerseFunction& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
unimplemented();
}
bool FVerseFunctionBinding::Diff(const FVerseFunction& A, const FVerseFunction& B, const FBindContext&)
{
//return !(A == B);
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
FDynamicallyTypedValueBinding::FDynamicallyTypedValueBinding(TPropertySpecifier<1>& Spec)
: MemberIds{GUE.Members.Value}
{
Spec.Members[0] = SpecDynamicStruct;
}
void FDynamicallyTypedValueBinding::Save(FMemberBuilder& Dst, const Type& Src, const Type* Default, const FSaveContext& Ctx) const
{
unimplemented();
}
void FDynamicallyTypedValueBinding::Load(Type& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
unimplemented();
}
bool FDynamicallyTypedValueBinding::Diff(const ::UE::FDynamicallyTypedValue& A, const ::UE::FDynamicallyTypedValue& B, const FBindContext&)
{
//return !UE::Verse::FRuntimeTypeDynamic::Get().AreIdentical(&A, &B);
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
FReferencePropertyBinding::FReferencePropertyBinding(TPropertySpecifier<1>& Spec)
: MemberIds{GUE.Members.Value}
{
Spec.Members[0] = SpecDynamicStruct;
}
void FReferencePropertyBinding::Save(FMemberBuilder& Dst, const FReferencePropertyValue& Src, const FReferencePropertyValue* Default, const FSaveContext& Ctx) const
{
unimplemented();
}
void FReferencePropertyBinding::Load(FReferencePropertyValue& Dst, FStructLoadView Src, ECustomLoadMethod Method) const
{
unimplemented();
}
bool FReferencePropertyBinding::Diff(const FReferencePropertyValue& A, const FReferencePropertyValue& B, const FBindContext&)
{
unimplemented();
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
struct FInterfaceBinding final : ICustomBinding
{
explicit FInterfaceBinding(UClass* Class) : InterfaceClass(Class) {}
const TObjectPtr<UClass> InterfaceClass;
virtual void SaveCustom(FMemberBuilder& Dst, const void* Src, const void* Default, const FSaveContext& Ctx) override
{
if (!Default || DiffCustom(Src, Default, Ctx))
{
Save(Dst, *static_cast<const FScriptInterface*>(Src), Ctx);
}
}
virtual void LoadCustom(void* Dst, FStructLoadView Src, ECustomLoadMethod Method) const override
{
check(Method == ECustomLoadMethod::Assign);
Load(*static_cast<FScriptInterface*>(Dst), Src);
}
virtual bool DiffCustom(const void* A, const void* B, const FBindContext&) const override
{
return *static_cast<const FScriptInterface*>(A) != *static_cast<const FScriptInterface*>(B);
}
void Save(FMemberBuilder& Dst, const FScriptInterface& Src, const FSaveContext& Ctx) const
{
const TObjectPtr<UObject>& ObjectRef = const_cast<FScriptInterface&>(Src).GetObjectRef();
Dst.AddStruct(GUE.Members.Object, GUE.Structs.ObjectPtr, SaveStruct(&ObjectRef, GUE.Structs.ObjectPtr, Ctx));
}
void Load(FScriptInterface& Dst, FStructLoadView Src) const
{
LoadSoleStruct(&Dst.GetObjectRef(), Src);
UObject* Object = Dst.GetObject();
Dst.SetInterface(Object ? Object->GetInterfaceAddress(InterfaceClass) : nullptr);
}
};
class FInterfaceBindings
{
const FType ScriptInterface;
FStructDeclarationPtr Declaration;
TMap<FType, FBindId> BoundClasses;
public:
FInterfaceBindings()
: ScriptInterface(GUE.Names.Resolve(GUE.Structs.ScriptInterface))
, Declaration(Declare({GUE.Structs.ScriptInterface, NoId, 0, EMemberPresence::RequireAll, {GUE.Members.Object}, {FMemberSpec(GUE.Structs.ObjectPtr)}}))
{}
FBindId Bind(FInterfaceProperty* Property)
{
FType Class = IndexType(Property->InterfaceClass);
if (const FBindId* BindId = BoundClasses.Find(Class))
{
return *BindId;
}
FType UniqueBindName = GUE.Names.MakeParametricType(ScriptInterface, {Class});
FBindId BindId = GUE.Names.IndexBindId(UniqueBindName);
BoundClasses.Emplace(Class, BindId);
// Todo: Ownership / memory leak
ICustomBinding* Leak = new FInterfaceBinding(Property->InterfaceClass);
GUE.Customs.BindStruct(BindId, *Leak, FStructDeclarationPtr(Declaration), {});
return BindId;
}
};
static FBindId BindInterface(FInterfaceProperty* Property)
{
static FInterfaceBindings Bindings;
return Bindings.Bind(Property);
}
////////////////////////////////////////////////////////////////////////////////////////////////
inline uint32 HashRangeBindings(TConstArrayView<FRangeBinding> In)
{
return static_cast<uint32>(FXxHash64::HashBuffer(In.GetData(), In.NumBytes()).Hash);
}
inline uint32 HashSkipOffset(FMemberBinding In)
{
uint32 Out = HashCombineFast(GetTypeHash(In.InnermostSchema), GetTypeHash(In.InnermostType));
return In.RangeBindings.IsEmpty() ? Out : HashCombineFast(Out, HashRangeBindings(In.RangeBindings));
}
inline bool EqSkipOffset(FMemberBinding A, FMemberBinding B)
{
return A.InnermostType == B.InnermostType && A.InnermostSchema == B.InnermostSchema && Algo::Compare(A.RangeBindings, B.RangeBindings);
}
// Helper to cache various property bindings instead of a TMap KeyFunc
struct FParameterBinding : FMemberBinding
{
friend uint32 GetTypeHash(FParameterBinding In) { return HashSkipOffset(In); };
inline bool operator==(FParameterBinding O) const { return EqSkipOffset(*this, O); }
};
////////////////////////////////////////////////////////////////////////////////////////////////
template<const std::string_view& Suffix>
static bool EndsWithDelimitedSuffix(FName EnumName, FName ValueName)
{
if (ValueName.GetNumber() != NAME_NO_NUMBER_INTERNAL)
{
return false;
}
// All type names and enum constants are ASCII
ANSICHAR Buffer[NAME_SIZE];
ValueName.GetComparisonNameEntry()->GetAnsiName(Buffer);
FAnsiStringView Value(Buffer);
if (Value.Len() >= Suffix.size() + 2 && Value.EndsWith(ToAnsiView(Suffix)))
{
// Todo: Check EnumName too, maybe based on ECppForm
char Delimiter = Value[Value.Len() - Suffix.size() - 1];
return Delimiter == ':' || Delimiter == '_';
}
return false;
}
static bool DenyMaxValue(FName Enum)
{
static const FName AllowsMax[] = {
"ESlateBrushMirrorType",
"EFortFeedbackAddressee",
"ECameraFocusMethod" };
return !Algo::Find(AllowsMax, Enum);
}
enum class ERoundtrip : uint8
{
None = 0,
PP = 1 << 0,
TPS = 1 << 1,
UPS = 1 << 2,
TextMemory = 1 << 3,
TextStable = 1 << 4,
};
ENUM_CLASS_FLAGS(ERoundtrip);
static FEnumId DeclareEnum(UEnum* Enum)
{
FType Type = IndexType(Enum);
FEnumId Id = GUE.Names.IndexEnum(Type);
EEnumMode Mode = Enum->HasAnyEnumFlags(EEnumFlags::Flags) ? EEnumMode::Flag : EEnumMode::Flat;
// Skip _MAX and _All enumerators
FName EnumName = Enum->GetFName();
int32 Num = Enum->NumEnums();
if (Num > 0 && DenyMaxValue(EnumName))
{
static constexpr std::string_view Max = "MAX";
static constexpr std::string_view All = "All";
Num -= EndsWithDelimitedSuffix<Max>(EnumName, Enum->GetNameByIndex(Num - 1));
Num -= Num > 0 && Mode == EEnumMode::Flag &&
EndsWithDelimitedSuffix<All>(EnumName, Enum->GetNameByIndex(Num - 1));
}
TArray<FEnumerator, TInlineAllocator<64>> Enumerators;
for (int32 Idx = 0; Idx < Num; ++Idx)
{
FName ValueName = Enum->GetNameByIndex(Idx);
Enumerators.Emplace(GUE.Names.MakeName(ValueName), static_cast<uint64>(Enum->GetValueByIndex(Idx)));
}
#if WITH_METADATA
// IsMax() classifies more names as "max" than Enum->ContainsExistingMax()
checkSlow(Enumerators.Num() == Num || Enum->ContainsExistingMax() || Enum->HasMetaData(TEXT("Hidden"), Num));
#endif
GUE.Enums.Declare(Id, Type, Mode, Enumerators, EEnumAliases::Strip);
return Id;
}
////////////////////////////////////////////////////////////////////////////////////////////////
inline uint64 NumBytes(int32 NumItems, SIZE_T ItemSize)
{
return static_cast<uint64>(NumItems) * ItemSize;
}
inline bool HasConstructor(const FProperty* Property)
{
return !(Property->PropertyFlags & CPF_ZeroConstructor);
}
inline bool HasDestructor(const FProperty* Property)
{
return !(Property->PropertyFlags & (CPF_IsPlainOldData | CPF_NoDestructor));
}
inline bool HasHash(const FProperty* Property)
{
return !!(Property->PropertyFlags & CPF_HasGetValueTypeHash);
}
inline void ConstructValue(const FProperty* Property, void* Value)
{
((*Property).*GInitPropertyValue)(Value);
}
inline void DestroyValue(const FProperty* Property, void* Value)
{
((*Property).*GDestroyPropertyValue)(Value);
}
inline uint32 HashValue(const FProperty* Property, const void* Item)
{
return ((*Property).*GHashPropertyValue)(Item);
}
inline void ConstructValues(const FProperty* Property, uint8* Values, int32 Num, SIZE_T Stride)
{
for (uint8* It = Values, *End = Values + Num*Stride; It != End; It += Stride)
{
((*Property).*GInitPropertyValue)(It);
}
}
inline void MemzeroStrided(uint8* Values, int32 Num, SIZE_T Size, SIZE_T Stride)
{
for (uint8* It = Values, *End = Values + Num*Stride; It != End; It += Stride)
{
FMemory::Memzero(It, Size);
}
}
inline void DestroyValues(const FProperty* Property, uint8* Values, int32 Num, SIZE_T Stride)
{
for (uint8* It = Values, *End = Values + Num*Stride; It != End; It += Stride)
{
((*Property).*GDestroyPropertyValue)(It);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Helps cache array property range bindings
struct FArrayPropertyInfo
{
explicit FArrayPropertyInfo(FArrayProperty* Property)
: bFreezable(!!(Property->ArrayFlags & EArrayPropertyFlags::UsesMemoryImageAllocator))
, bDestructor(HasDestructor(Property->Inner))
, bConstructor(HasConstructor(Property->Inner))
, ItemAlign(Property->Inner->GetMinAlignment())
, ItemSize(Property->Inner->GetElementSize())
{}
union
{
struct
{
uint32 bFreezable : 1;
uint32 bDestructor : 1;
uint32 bConstructor : 1;
uint32 ItemAlign : 29;
};
uint32 Int;
};
uint32 ItemSize;
bool IsTrivial() const { return !bDestructor && !bConstructor; }
bool operator==(FArrayPropertyInfo O) const { return Int == O.Int && ItemSize == O.ItemSize; }
friend uint32 GetTypeHash(FArrayPropertyInfo I) { return HashCombineFast(I.Int, I.ItemSize); };
};
static_assert(sizeof(FArrayPropertyInfo) == 8);
// Cacheable FArrayProperty binding
template<class ScriptArray>
struct TTrivialArrayBinding : IItemRangeBinding
{
const FArrayPropertyInfo Info;
TTrivialArrayBinding(FArrayPropertyInfo InFo, FConcreteTypenameId BindName = GUE.Typenames.TrivialArray)
: IItemRangeBinding(BindName)
, Info(InFo) {}
virtual void ReadItems(FSaveRangeContext& Ctx) const override
{
const ScriptArray& Array = Ctx.Request.GetRange<ScriptArray>();
Ctx.Items.SetAll(Array.GetData(), static_cast<uint64>(Array.Num()), Info.ItemSize);
}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
ScriptArray& Array = Ctx.Request.GetRange<ScriptArray>();
int32 NewNum = static_cast<int32>(Ctx.Request.NumTotal());
Array.SetNumUninitialized(NewNum, Info.ItemSize, Info.ItemAlign);
if (NewNum)
{
FMemory::Memzero(Array.GetData(), NumBytes(NewNum, Info.ItemSize));
}
Ctx.Items.Set(Array.GetData(), static_cast<uint64>(NewNum), Info.ItemSize);
}
};
// Currently can't extract constructor/destructor function pointers from FProperty, which
// requires keeping FProperty* and prevents range binding reuse, @see AllocateArrayBinding()
template<class ScriptArray>
struct TNonTrivialArrayBinding : TTrivialArrayBinding<ScriptArray>
{
const FProperty* Inner;
using TTrivialArrayBinding<ScriptArray>::Info;
TNonTrivialArrayBinding(FArrayPropertyInfo InFo, const FProperty* InNer)
: TTrivialArrayBinding<ScriptArray>(InFo, GUE.Typenames.NonTrivialArray)
, Inner(InNer)
{}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
ScriptArray& Array = Ctx.Request.GetRange<ScriptArray>();
int32 NumDestroy = Info.bDestructor * Array.Num();
DestroyValues(Inner, static_cast<uint8*>(Array.GetData()), NumDestroy, Info.ItemSize);
uint64 NewNum = Ctx.Request.NumTotal();
Array.SetNumUninitialized(static_cast<int32>(NewNum), Info.ItemSize, Info.ItemAlign);
InitItems(NewNum, Array.GetData());
Ctx.Items.Set(Array.GetData(), NewNum, Info.ItemSize);
}
inline void InitItems(uint64 Num, void* Items) const
{
if (Info.bConstructor)
{
ConstructValues(Inner, static_cast<uint8*>(Items), Num, Info.ItemSize);
}
else if (Num)
{
FMemory::Memzero(Items, Num * Info.ItemSize);
}
}
};
template<ELeafType Type, ELeafWidth Width>
struct TLeafArrayBinding : ILeafRangeBinding
{
inline static constexpr SIZE_T LeafSize = SizeOf(Width);
TLeafArrayBinding() : ILeafRangeBinding(GUE.Typenames.LeafArray) {}
virtual void SaveLeaves(const void* Range, FLeafRangeAllocator& Out) const override
{
const FScriptArray& Array = *static_cast<const FScriptArray*>(Range);
if (int32 Num = Array.Num())
{
void* Dst = Out.AllocateNonEmptyRange(Num, Width);
FMemory::Memcpy(Dst, Array.GetData(), NumBytes(Num));
}
}
virtual void LoadLeaves(void* Range, FLeafRangeLoadView Leaves) const override
{
FScriptArray& Array = *static_cast<FScriptArray*>(Range);
Array.SetNumUninitialized(static_cast<int32>(Leaves.Num()), LeafSize, LeafSize);
Leaves.AsBitCast<Type, Width>().Copy(Array.GetData(), NumBytes(Array.Num()));
}
virtual bool DiffLeaves(const void* RangeA, const void* RangeB) const override
{
const FScriptArray& A = *static_cast<const FScriptArray*>(RangeA);
const FScriptArray& B = *static_cast<const FScriptArray*>(RangeB);
return Diff(A.Num(), B.Num(), A.GetData(), B.GetData(), LeafSize);
}
inline constexpr uint64 NumBytes(int32 NumItems) const
{
return static_cast<uint64>(NumItems) * LeafSize;
}
};
// Reusable cache of FArrayProperty range bindings
class FArrayPropertyBindings
{
static constexpr ERangeSizeType SizeType = DefaultRangeMax;
const TLeafArrayBinding<ELeafType::Bool, ELeafWidth::B8> Bool;
const TLeafArrayBinding<ELeafType::Float, ELeafWidth::B32> Float;
const TLeafArrayBinding<ELeafType::Float, ELeafWidth::B64> Double;
const TLeafArrayBinding<ELeafType::IntS, ELeafWidth::B8> IntS8;
const TLeafArrayBinding<ELeafType::IntS, ELeafWidth::B16> IntS16;
const TLeafArrayBinding<ELeafType::IntS, ELeafWidth::B32> IntS32;
const TLeafArrayBinding<ELeafType::IntS, ELeafWidth::B64> IntS64;
const TLeafArrayBinding<ELeafType::IntU, ELeafWidth::B8> IntU8;
const TLeafArrayBinding<ELeafType::IntU, ELeafWidth::B16> IntU16;
const TLeafArrayBinding<ELeafType::IntU, ELeafWidth::B32> IntU32;
const TLeafArrayBinding<ELeafType::IntU, ELeafWidth::B64> IntU64;
const FRangeBinding Integers[2][4];
TMap<FArrayPropertyInfo, IItemRangeBinding*> Others;
inline const ILeafRangeBinding& DownCast(const ILeafRangeBinding& In) { return In; }
public:
FArrayPropertyBindings()
: Integers{{FRangeBinding(IntU8, SizeType), FRangeBinding(IntU16, SizeType), FRangeBinding(IntU32, SizeType), FRangeBinding(IntU64, SizeType)},
{FRangeBinding(IntS8, SizeType), FRangeBinding(IntS16, SizeType), FRangeBinding(IntS32, SizeType), FRangeBinding(IntS64, SizeType)}}
{}
~FArrayPropertyBindings()
{
for (TPair<FArrayPropertyInfo, IItemRangeBinding*>& Cached : Others)
{
FMemory::Free(Cached.Value);
}
}
FRangeBinding RangeBind(FArrayPropertyInfo Info, uint64 InnerCastFlags)
{
if (HasAny<LeafMask>(InnerCastFlags) && !Info.bFreezable)
{
uint32 SizeIdx = FMath::FloorLog2NonZero(Info.ItemSize);
check(SizeIdx < 4);
// Note that we throw away enum schema, only size not needed to load/save enums
if (HasAny<IntSMask | IntUMask | CASTCLASS_FEnumProperty>(InnerCastFlags))
{
return Integers[HasAny<IntSMask>(InnerCastFlags)][SizeIdx];
}
check(HasAny<CASTCLASS_FFloatProperty | CASTCLASS_FDoubleProperty | CASTCLASS_FBoolProperty >(InnerCastFlags));
const ILeafRangeBinding& Binding = HasAny<CASTCLASS_FBoolProperty>(InnerCastFlags)
? DownCast(Bool) : (HasAny<CASTCLASS_FFloatProperty>(InnerCastFlags) ? DownCast(Float) : Double);
return FRangeBinding(Binding, SizeType);
}
else if (IItemRangeBinding** Cached = Others.Find(Info))
{
return FRangeBinding(**Cached, SizeType);
}
IItemRangeBinding* New = Info.bFreezable ? CreateAndCache<FFreezableScriptArray>(Info)
: CreateAndCache<FScriptArray>(Info);
return FRangeBinding(*New, SizeType);
}
template<typename ScriptArray>
IItemRangeBinding* CreateAndCache(FArrayPropertyInfo Info)
{
static_assert(std::is_trivially_destructible_v<TTrivialArrayBinding<ScriptArray>>);
return Others.Emplace(Info, new TTrivialArrayBinding<ScriptArray>(Info));
}
};
static FArrayPropertyBindings GCachedArrayBindings;
template<typename ScriptArray>
IItemRangeBinding* CreateAndLeak(FArrayPropertyInfo Info, FProperty* Inner)
{
return new TNonTrivialArrayBinding<ScriptArray>(Info, Inner);
}
static FRangeBinding AllocateArrayBinding(FArrayProperty* Property)
{
FProperty* Inner = Property->Inner;
FArrayPropertyInfo Info(Property);
if (Info.IsTrivial())
{
return GCachedArrayBindings.RangeBind(Info, Inner->GetCastFlags());
}
// Todo: Ownership / memory leak, try make non-trivial case cacheable by making FProperty ctor/dtor extractable
IItemRangeBinding* Out = Info.bFreezable ? CreateAndLeak<FFreezableScriptArray>(Info, Inner)
: CreateAndLeak<FScriptArray>(Info, Inner);
return FRangeBinding(*Out, ERangeSizeType::S32);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers to avoid using leaf FProperty instances after binding
//
// Below must match FFloatProperty, FDoubleProperty, FBoolProperty, FEnumProperty, TNumericProperty
// Identical() and GetValueTypeHashInternal() implementations perfectly except not supporting nullptrs
inline uint32 LeafPropertyHash(float In) { return ::UE::PreciseFPHash(In); }
inline uint32 LeafPropertyHash(double In) { return ::UE::PreciseFPHash(In); }
inline bool LeafPropertyIdentical(float A, float B) { return ::UE::PreciseFPEqual(A, B); }
inline bool LeafPropertyIdentical(double A, double B) { return ::UE::PreciseFPEqual(A, B); }
template<typename T>
inline uint32 LeafPropertyHash(T In) requires (std::is_unsigned_v<T>) { return GetTypeHash(In); }
template<typename T>
inline bool LeafPropertyIdentical(T A, T B) requires (std::is_unsigned_v<T>) { return A == B; }
// Type-erased just enough to call LeafPropertyHash / LeafPropertyIdentical and FLeafRangeLoadView::As/AsBitcast
enum class EPropertyKind : uint8 { Range, Struct, Bool, U8, U16, U32, U64, F32, F64 };
template<EPropertyKind Kind>
struct TEquivalentLeafType { using Type = void; };
template<> struct TEquivalentLeafType<EPropertyKind::Bool> { using Type = bool; };
template<> struct TEquivalentLeafType<EPropertyKind::U8> { using Type = uint8; };
template<> struct TEquivalentLeafType<EPropertyKind::U16> { using Type = uint16; };
template<> struct TEquivalentLeafType<EPropertyKind::U32> { using Type = uint32; };
template<> struct TEquivalentLeafType<EPropertyKind::U64> { using Type = uint64; };
template<> struct TEquivalentLeafType<EPropertyKind::F32> { using Type = float; };
template<> struct TEquivalentLeafType<EPropertyKind::F64> { using Type = double; };
template<EPropertyKind Kind>
using EquivalentLeafType = typename TEquivalentLeafType<Kind>::Type;
template<typename LeafType>
uint32 LeafHash(const void* In)
{
return LeafPropertyHash(*static_cast<const LeafType*>(In));
}
template<typename LeafType>
bool LeafIdentical(const void* A, const void* B)
{
return LeafPropertyIdentical(*static_cast<const LeafType*>(A), *static_cast<const LeafType*>(B));
}
template<typename LeafType>
inline auto CastAs(FLeafRangeLoadView In)
{
if constexpr (std::is_floating_point_v<LeafType>)
{
return In.As<LeafType>();
}
else
{
return In.AsBitCast<LeafType>();
}
}
inline EPropertyKind GetPropertyKind(FLeafBindType In)
{
ELeafType Type = ToLeafType(In.Bind.Type);
ELeafWidth Width = In.Basic.Width;
if (Type == ELeafType::Float)
{
return Width == ELeafWidth::B32 ? EPropertyKind::F32 : EPropertyKind::F64;
}
else if (Type == ELeafType::Bool)
{
return EPropertyKind::Bool;
}
else switch (Width)
{
case ELeafWidth::B8: return EPropertyKind::U8;
case ELeafWidth::B16: return EPropertyKind::U16;
case ELeafWidth::B32: return EPropertyKind::U32;
default: return EPropertyKind::U64;
}
}
static EPropertyKind GetPropertyKind(FMemberBinding In)
{
return (In.RangeBindings.Num() > 0) ? EPropertyKind::Range
: In.InnermostType.IsStruct()
? EPropertyKind::Struct
: GetPropertyKind(In.InnermostType.AsLeaf());
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Inner leaf property, e.g. FEnumProperty, FNumericProperty
template<Arithmetic EquivalentType>
struct TInnerLeafProperty
{
static constexpr EMemberKind Kind = EMemberKind::Leaf;
static constexpr bool bConstruct = false;
static constexpr bool bDestruct = false;
static constexpr bool bHashable = true;
static constexpr int32 Size = sizeof(EquivalentType);
TInnerLeafProperty(FProperty* In) { check(Size == In->GetElementSize());}
inline static void InitItem(void* In) {} // Note doesn't zero out items about to be overwritten
inline static void DestroyItem(void* In) {}
inline static EquivalentType Cast(const void* In) { return *static_cast<const EquivalentType*>(In); } // Todo: Consider if BitCast is needed
inline static uint32 Hash(const void* In) { return LeafPropertyHash(Cast(In)); }
inline static bool Identical(const void* A, const void* B) { return LeafPropertyIdentical(Cast(A), Cast(B)); }
};
// Inner range-bound property, e.g. FArrayProperty, FStringProperty, FSetProperty
struct FInnerRangeProperty
{
static constexpr EMemberKind Kind = EMemberKind::Range;
static constexpr bool bConstruct = false;
static constexpr bool bDestruct = true;
FProperty* Property;
uint32 Size;
bool bHashable;
FInnerRangeProperty(FProperty* In)
: Property(In)
, Size(In->GetElementSize())
, bHashable(HasHash(In))
{
check(!HasConstructor(In));
check(HasDestructor(In));
}
inline void InitItem(void* In) const { FMemory::Memzero(In, Size); }
inline void DestroyItem(void* In) const { DestroyValue(Property, In); }
};
// Inner struct-bound property, e.g. FStructProperty, FNameProperty, FObjectProperty
struct FInnerStructProperty
{
static constexpr EMemberKind Kind = EMemberKind::Struct;
FProperty* Property;
uint32 Size;
bool bConstruct;
bool bDestruct;
bool bHashable;
FInnerStructProperty(FProperty* In)
: Property(In)
, Size(In->GetElementSize())
, bConstruct(HasConstructor(In))
, bDestruct(HasDestructor(In))
, bHashable(HasHash(In))
{}
inline void InitItem(void* Item) const
{
if (bConstruct)
{
ConstructValue(Property, Item);
}
else
{
FMemory::Memzero(Item, Size);
}
}
inline void DestroyItem(void* Item) const
{
if (bDestruct)
{
DestroyValue(Property, Item);
}
}
};
template<EPropertyKind Kind> struct TSelectInnerProperty { using Type = TInnerLeafProperty<EquivalentLeafType<Kind>>; };
template<> struct TSelectInnerProperty<EPropertyKind::Range> { using Type = FInnerRangeProperty; };
template<> struct TSelectInnerProperty<EPropertyKind::Struct> { using Type = FInnerStructProperty; };
template<EPropertyKind Kind>
using TInnerProperty = typename TSelectInnerProperty<Kind>::Type;
template<typename InnerPropertyType>
inline auto MakeHashFn(const InnerPropertyType& Inner)
{
if constexpr (InnerPropertyType::Kind == EMemberKind::Leaf)
{
return &InnerPropertyType::Hash;
}
else
{
return [P = Inner.Property](const void* In) { return HashValue(P, In); };
}
}
template<typename InnerPropertyType>
inline auto MakeIdenticalFn(const InnerPropertyType& Inner)
{
if constexpr (InnerPropertyType::Kind == EMemberKind::Leaf)
{
return &InnerPropertyType::Identical;
}
else
{
return [P = Inner.Property](const void* A, const void* B) { return P->Identical(A, B); };
}
}
template<typename InnerPropertyType>
inline void InitStridedItems(const InnerPropertyType& Inner, void* Items, uint64 Num, SIZE_T Stride)
{
if constexpr (InnerPropertyType::Kind == EMemberKind::Leaf)
{}
else if (Inner.bConstruct)
{
ConstructValues(Inner.Property, static_cast<uint8*>(Items), Num, Stride);
}
else
{
MemzeroStrided(static_cast<uint8*>(Items), Num, Inner.Size, Stride);
}
}
template<typename InnerPropertyType>
inline void DestroyStridedItems(const InnerPropertyType& Inner, void* Items, uint64 Num, SIZE_T Stride)
{
if constexpr (InnerPropertyType::Kind == EMemberKind::Leaf)
{}
else if (Inner.bDestruct)
{
DestroyValues(Inner.Property, static_cast<uint8*>(Items), Num, Stride);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
template<typename EquivalentType>
struct TLeafRangeSerializer
{
using RangeSaver = TLeafRangeSaver<EquivalentType>;
static constexpr SIZE_T Size = sizeof(EquivalentType);
FMemberType InnerType;
FOptionalInnerId EnumId;
explicit TLeafRangeSerializer(FMemberBinding In)
: InnerType(ToLeafType(In.InnermostType.AsLeaf()))
, EnumId(In.InnermostSchema)
{
check(In.RangeBindings.IsEmpty());
check(InnerType.AsLeaf().Width == WidthOf(Size));
}
inline static EquivalentType Cast(const void* In)
{
return *static_cast<const EquivalentType*>(In);
}
inline FMemberSchema MakeMemberSchema() const
{
return { FMemberType(DefaultRangeMax), InnerType, 1, EnumId, nullptr };
}
inline FMemberSpec SpecMember() const
{
return DefaultRangeOf(FMemberSpec(InnerType, FOptionalInnerId(EnumId)));
}
inline static EquivalentType SaveItem(const void* In, const FSaveContext&)
{
return Cast(In);
}
inline static void LoadItem(void* Dst, FByteReader& SrcBytes, FBitCacheReader& SrcBits, FOptionalSchemaId, const FLoadBatch&) requires (std::is_same_v<EquivalentType, bool>)
{
*static_cast<bool*>(Dst) = SrcBits.GrabNext(SrcBytes);
}
inline static void LoadItem(void* Dst, FByteReader& SrcBytes, FBitCacheReader&, FOptionalSchemaId, const FLoadBatch&)
{
*static_cast<EquivalentType*>(Dst) = Cast(SrcBytes.GrabBytes(Size));
}
};
struct FStructRangeSerializer
{
using RangeSaver = FStructRangeSaver;
FMemberType InnerType;
FBindId SaveId;
explicit FStructRangeSerializer(FMemberBinding Item)
: InnerType(Item.InnermostType.AsStruct())
, SaveId(Item.InnermostSchema.Get().AsStruct())
{
check(Item.RangeBindings.IsEmpty());
}
inline FMemberSchema MakeMemberSchema() const
{
return { FMemberType(DefaultRangeMax), InnerType, 1, FInnerId(SaveId), nullptr };
}
inline FMemberSpec SpecMember() const
{
// Lowercast is safe since FProperty doesnt support type-erasure
return DefaultRangeOf(LowerCast(SaveId));
}
inline FBuiltStruct* SaveItem(const void* In, const FSaveContext& Ctx) const
{
return SaveStruct(In, SaveId, Ctx);
}
void LoadItem(void* Dst, FByteReader& SrcBytes, FBitCacheReader&, FOptionalSchemaId LoadId, const FLoadBatch& Batch) const
{
LoadStruct(Dst, FByteReader(SrcBytes.GrabSkippableSlice()), static_cast<FStructSchemaId>(LoadId.Get()), Batch);
}
};
struct FNestedRangeSerializer
{
using RangeSaver = FNestedRangeSaver;
FOptionalInnerId InnermostSaveId;
uint16 NumInners;
TArray<FMemberType, TInlineAllocator<8>> InnerTypes;
TArray<FMemberBindType, TInlineAllocator<8>> InnerBindTypes;
TArray<FRangeBinding, TInlineAllocator<2>> InnerBindings;
explicit FNestedRangeSerializer(FMemberBinding Item)
: InnermostSaveId(Item.InnermostSchema)
, NumInners(IntCastChecked<uint16>(1 + Item.RangeBindings.Num()))
, InnerBindings(Item.RangeBindings)
{
check(NumInners >= 2);
for (FRangeBinding Inner : Item.RangeBindings)
{
InnerTypes.Emplace(Inner.GetSizeType());
InnerBindTypes.Emplace(Inner.GetSizeType());
}
InnerTypes.Emplace(Item.InnermostType.IsStruct() ? FMemberType(Item.InnermostType.AsStruct())
: FMemberType(ToLeafType(Item.InnermostType.AsLeaf())));
InnerBindTypes.Emplace(Item.InnermostType);
}
inline FMemberSchema MakeMemberSchema() const
{
return { FMemberType(DefaultRangeMax), InnerTypes[0], NumInners, InnermostSaveId, InnerTypes.GetData() };
}
inline FMemberSpec SpecMember() const
{
// InnermostSaveId is safe since FProperty doesnt support type-erasure
return FMemberSpec(InnerTypes, InnermostSaveId);
}
FBuiltRange* SaveItem(const void* In, const FSaveContext& Ctx) const
{
FRangeMemberBinding Member = { InnerBindTypes.GetData() + 1, InnerBindings.GetData(), NumInners - 1, InnermostSaveId, 0 };
return SaveRange(In, Member, Ctx);
}
inline void LoadItem(void* Dst, FByteReader& SrcBytes, FBitCacheReader& SrcBits, FOptionalSchemaId InnermostLoadId, const FLoadBatch& Batch) const
{
FRangeLoadSchema Schema = { InnerTypes[1], InnermostLoadId, InnerTypes.GetData() + 2, Batch};
LoadRange(Dst, SrcBytes, SrcBits, DefaultRangeMax, Schema, InnerBindings);
}
};
template<EPropertyKind Kind> struct TSelectRangeSerializer { using Type = TLeafRangeSerializer<EquivalentLeafType<Kind>>; };
template<> struct TSelectRangeSerializer<EPropertyKind::Range> { using Type = FNestedRangeSerializer; };
template<> struct TSelectRangeSerializer<EPropertyKind::Struct> { using Type = FStructRangeSerializer; };
template<EPropertyKind Kind>
using TPropertyRangeSerializer = typename TSelectRangeSerializer<Kind>::Type;
////////////////////////////////////////////////////////////////////////////////////////////////
inline FScriptSparseArray& AsSparseArray(FScriptSet& In) { return reinterpret_cast<FScriptSparseArray&>(In); }
inline FScriptSparseArray& AsSparseArray(FScriptMap& In) { return reinterpret_cast<FScriptSparseArray&>(In); }
template<typename ScriptSet>
inline bool IsCompact(const ScriptSet& Set)
{
return Set.NumUnchecked() == Set.GetMaxIndex();
}
// There's no TScriptSparseArray::SetNumUninitialized() (yet),
// reserve using Empty() and add items one by one instead
template<class ScriptType, class LayoutType>
uint8* SetNumUninitialized(ScriptType& Dst, const LayoutType& Layout, uint64 Num)
{
check(Dst.IsEmpty());
Dst.Empty(static_cast<int32>(Num), Layout);
for (uint64 Idx = 0; Idx < Num; ++Idx)
{
Dst.AddUninitialized(Layout);
}
check(IsCompact(Dst));
return static_cast<uint8*>(Dst.GetData(0, Layout));
}
// @pre Elems.Num() > 0
inline FExistingItemSlice GetContiguousSlice(int32 Idx, const FScriptSparseArray& Elems, const uint8* Data, SIZE_T Stride)
{
checkSlow(!Elems.IsEmpty());
int32 Num = 1;
for (;!Elems.IsValidIndex(Idx); ++Idx) { checkSlow(Idx < Elems.GetMaxIndex()); }
for (; Elems.IsValidIndex(Idx + Num); ++Num) {}
return { Data + NumBytes(Idx, Stride), static_cast<uint64>(Num) };
}
// Save flat TSet/TMap
inline void ReadSparseItems(FExistingItems& Dst, const FScriptSparseArray& Src, const FScriptSparseArrayLayout& Layout)
{
const uint8* Data = static_cast<const uint8*>(Src.GetData(0, Layout));
if (Src.IsEmpty())
{
Dst.SetAll(nullptr, 0, Layout.Size);
}
else if (FExistingItemSlice LastRead = Dst.Slice)
{
// Continue partial response
int64 PriorBytesRead = static_cast<const uint8*>(LastRead.Data) - Data;
check(PriorBytesRead % Layout.Size == 0);
int32 LastIdx = PriorBytesRead / Layout.Size;
int32 NextIdx = LastIdx + LastRead.Num + /* skip one known invalid */ 1;
check(NextIdx < Src.GetMaxIndex());
Dst.Slice = GetContiguousSlice(NextIdx, Src, Data, Layout.Size);
}
else if (Src.IsCompact())
{
Dst.SetAll(Data, static_cast<uint64>(Src.Num()), Layout.Size);
}
else
{
// Start partial response
Dst.NumTotal = Src.Num();
Dst.Stride = Layout.Size;
Dst.Slice = GetContiguousSlice(0, Src, Data, Layout.Size);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Helps save TSet/TMap deltas
template<class ScriptType, class LayoutType>
struct TSubSetIterator
{
const LayoutType Layout;
const ScriptType& Set;
const TBitArray<>& Subset;
const int32 Max;
int32 Idx;
TSubSetIterator(const LayoutType& InLayout, const ScriptType& InSet, const TBitArray<>& InSubset)
: Layout(InLayout)
, Set(InSet)
, Subset(InSubset)
, Max(InSet.GetMaxIndex())
, Idx(Max > 0 ? Subset.Find(true) : INDEX_NONE)
{}
explicit operator bool() const { return Idx != INDEX_NONE; }
const void* operator*() const { return Set.GetData(Idx, Layout); }
void operator++() { Idx = ++Idx < Max ? Subset.FindFrom(true, Idx) : INDEX_NONE; }
uint32 CountNum() const { return Subset.CountSetBits(); }
};
// Helps save TSet/TMap deltas
template<class ScriptType, class LayoutType, class SerializerType>
FTypedRange SaveAll(const ScriptType& Set, const LayoutType& Layout, const SerializerType& Serializer, const FSaveContext& Ctx)
{
if (Set.IsEmpty())
{
return { Serializer.MakeMemberSchema(), nullptr };
}
typename SerializerType::RangeSaver Range(Ctx.Scratch, static_cast<uint64>(Set.Num()));
for (int32 Idx = 0, Max = Set.GetMaxIndex(); Idx < Max; ++Idx)
{
if (Set.IsValidIndex(Idx))
{
Range.AddItem(Serializer.SaveItem(Set.GetData(Idx, Layout), Ctx));
}
}
return Range.Finalize(Serializer.MakeMemberSchema());
}
// Helps save TSet/TMap deltas
template<class SubSetIteratorType, class SerializerType>
FTypedRange SaveSome(SubSetIteratorType& It, const SerializerType& Serializer, const FSaveContext& Ctx)
{
typename SerializerType::RangeSaver Range(Ctx.Scratch, It.CountNum());
for (; It; ++It)
{
Range.AddItem(Serializer.SaveItem(*It, Ctx));
}
return Range.Finalize(Serializer.MakeMemberSchema());
}
template<typename BindingType, typename ScriptType>
void SaveSetDelta(const BindingType& Binding, FMemberBuilder& Dst, const ScriptType& Src, const ScriptType* Default, const FSaveContext& Ctx)
{
if (!Default)
{
Dst.AddRange(GUE.Members.Assign, SaveAll(Src, Binding.Layout, Binding.GetItemRange(), Ctx));
}
else if (Default->IsEmpty())
{
if (!Src.IsEmpty())
{
Dst.AddRange(GUE.Members.Insert, SaveAll(Src, Binding.Layout, Binding.GetItemRange(), Ctx));
}
}
else if (Src.IsEmpty())
{
Dst.AddRange(GUE.Members.Remove, SaveAll(*Default, Binding.Layout, Binding.GetKeyRange(), Ctx));
}
else // Neither are empty
{
TBitArray<> RemoveIds(false, Default->GetMaxIndex());
for (int32 Idx = 0, Max = Default->GetMaxIndex(); Idx < Max; ++Idx)
{
RemoveIds[Idx] = Default->IsValidIndex(Idx) && !Binding.HasKey(Src, Default->GetData(Idx, Binding.Layout));
}
if (typename BindingType::SubSetIterator Removed{Binding.Layout, *Default, RemoveIds})
{
Dst.AddRange(GUE.Members.Remove, SaveSome(Removed, Binding.GetKeyRange(), Ctx));
}
TBitArray<> InsertIds(false, Src.GetMaxIndex());
for (int32 Idx = 0, Max = Src.GetMaxIndex(); Idx < Max; ++Idx)
{
InsertIds[Idx] = Src.IsValidIndex(Idx) && !Binding.HasItem(*Default, Src.GetData(Idx, Binding.Layout));
}
if (typename BindingType::SubSetIterator Inserted{Binding.Layout, Src, InsertIds})
{
Dst.AddRange(GUE.Members.Insert, SaveSome(Inserted, Binding.GetItemRange(), Ctx));
}
}
}
template<typename BindingType, typename ScriptType>
void InsertSetItems(const BindingType& Binding, ScriptType& Dst, FRangeLoadView Items)
{
// Insert
if (Dst.IsEmpty())
{
Binding.AssignEmpty(Dst, Items);
}
else
{
Binding.InsertNonEmpty(Dst, Items);
}
}
template<typename BindingType, typename ScriptType>
void LoadSetDelta(const BindingType& Binding, ScriptType& Dst, FStructLoadView Src)
{
FMemberLoader Members(Src);
FOptionalMemberId Name = Members.PeekName();
FRangeLoadView Range = Members.GrabRange();
if (Name == GUE.Members.Insert)
{
InsertSetItems(Binding, Dst, Range);
}
else if (Name == GUE.Members.Assign)
{
Binding.DestroyAll(Dst);
Binding.AssignEmpty(Dst, Range);
}
else
{
checkSlow(Name == GUE.Members.Remove);
Binding.Remove(Dst, Range);
if (Members.HasMore())
{
checkSlow(Members.PeekNameUnchecked() == GUE.Members.Insert);
InsertSetItems(Binding, Dst, Members.GrabRange());
}
}
checkSlow(!Members.HasMore());
}
template<typename BindingType, typename ScriptType>
inline bool DiffSet(const BindingType& Binding, const ScriptType& A, const ScriptType& B)
{
if (A.NumUnchecked() != B.NumUnchecked())
{
return true;
}
if (A.NumUnchecked() > 0)
{
for (int32 IdxA = 0, MaxA = A.GetMaxIndex(); IdxA < MaxA; ++IdxA)
{
if (A.IsValidIndex(IdxA) && !Binding.HasItem(B, A.GetData(IdxA, Binding.Layout)))
{
return true;
}
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
template<EPropertyKind ElemKind>
struct TSetPropertyBinding : IItemRangeBinding, ICustomBinding
{
using SetType = FScriptSet;
using LeafType = EquivalentLeafType<ElemKind>;
using SubSetIterator = TSubSetIterator<FScriptSet, FScriptSetLayout>;
static constexpr bool bLeaves = !std::is_void_v<LeafType>;
const FScriptSetLayout Layout;
const TInnerProperty<ElemKind> Inner;
const TPropertyRangeSerializer<ElemKind> Range;
const TPropertyRangeSerializer<ElemKind>& GetKeyRange() const { return Range; }
const TPropertyRangeSerializer<ElemKind>& GetItemRange() const { return Range; }
TSetPropertyBinding(FSetProperty* In, FMemberBinding Elem)
: IItemRangeBinding(GUE.Typenames.Set)
, Layout(In->SetLayout)
, Inner(In->ElementProp)
, Range(Elem)
{
check(Layout.Size == GetStride());
check(Inner.bHashable);
}
inline FStructDeclarationPtr DeclareCustom(FDeclId Id) const
{
FMemberId Members[] = { GUE.Members.Assign, GUE.Members.Remove, GUE.Members.Insert };
FMemberSpec RangeSpec = Range.SpecMember();
return Declare({Id, NoId, 0, EMemberPresence::AllowSparse, Members, {RangeSpec, RangeSpec, RangeSpec}});
}
inline SIZE_T GetStride() const requires (bLeaves) { return sizeof(TSetElement<LeafType>); }
inline SIZE_T GetStride() const { return Layout.Size; }
inline int32 FindIndex(const FScriptSet& Set, const void* Elem) const
{
return Set.FindIndex(Elem, Layout, MakeHashFn(Inner), MakeIdenticalFn(Inner));
}
inline void RemoveElem(FScriptSet& Set, const void* Elem) const
{
if (int32 Idx = FindIndex(Set, Elem); Idx != INDEX_NONE)
{
DestroyElem(Set, Idx);
Set.RemoveAt(Idx, Layout);
}
}
inline bool HasItem(const FScriptSet& Set, const void* Elem) const
{
return FindIndex(Set, Elem) != INDEX_NONE;
}
inline bool HasKey(const FScriptSet& Set, const void* Elem) const
{
return HasItem(Set, Elem);
}
inline void Rehash(FScriptSet& Set) const
{
Set.Rehash(Layout, MakeHashFn(Inner));
}
inline void DestroyElem(FScriptSet& Set) const requires (bLeaves) {}
inline void DestroyElem(FScriptSet& Set, int32 Idx) const
{
Inner.DestroyItem(Set.GetData(Idx, Layout));
}
inline void DestroyAll(FScriptSet& Set) const requires (bLeaves) {}
inline void DestroyAll(FScriptSet& Set) const
{
uint8* It = static_cast<uint8*>(Set.GetData(0, Layout));
SIZE_T Stride = GetStride();
if (IsCompact(Set))
{
DestroyStridedItems(Inner, It, Set.NumUnchecked(), Stride);
}
else
{
for (int32 Idx = 0, Max = Set.GetMaxIndex(); Idx < Max; ++Idx)
{
if (Set.IsValidIndex(Idx))
{
DestroyValue(Inner.Property, It);
}
It += Stride;
}
}
}
virtual void ReadItems(FSaveRangeContext& Ctx) const override
{
ReadSparseItems(/* out */ Ctx.Items, Ctx.Request.GetRange<FScriptSparseArray>(), Layout.SparseArrayLayout);
}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
FScriptSet& Set = Ctx.Request.GetRange<FScriptSet>();
int32 NewNum = static_cast<int32>(Ctx.Request.NumTotal());
if (Ctx.Request.IsFirstCall())
{
DestroyAll(Set);
Set.Empty(NewNum, Layout);
if (NewNum)
{
uint8* Items = SetNumUninitialized(Set, Layout, NewNum);
InitStridedItems(Inner, Items, NewNum, GetStride());
Ctx.Items.Set(Items, Ctx.Request.NumTotal(), GetStride());
Ctx.Items.RequestFinalCall();
}
}
else
{
check(Ctx.Request.IsFinalCall());
Rehash(Set);
}
}
virtual void SaveCustom(FMemberBuilder& Dst, const void* Src, const void* Default, const FSaveContext& Ctx) override
{
SaveSetDelta(*this, Dst, *static_cast<const FScriptSet*>(Src), static_cast<const FScriptSet*>(Default), Ctx);
}
virtual void LoadCustom(void* Dst, FStructLoadView Src, ECustomLoadMethod Method) const override
{
check(Method == ECustomLoadMethod::Assign);
LoadSetDelta(*this, *static_cast<FScriptSet*>(Dst), Src);
}
virtual bool DiffCustom(const void* A, const void* B, const FBindContext&) const override
{
return DiffSet(*this, *static_cast<const FScriptSet*>(A), *static_cast<const FScriptSet*>(B));
}
// Load into empty set
inline void AssignEmpty(FScriptSet& Dst, FRangeLoadView Src) const
{
SIZE_T Stride = GetStride();
uint8* It = SetNumUninitialized(Dst, Layout, Src.Num());
InitStridedItems(Inner, It, Src.Num(), Stride);
if constexpr (bLeaves)
{
for (LeafType Item : CastAs<LeafType>(Src.AsLeaves()))
{
*reinterpret_cast<LeafType*>(It) = Item;
It += Stride;
}
}
else if constexpr (ElemKind == EPropertyKind::Range)
{
TConstArrayView<FRangeBinding> InnerBindings = Range.InnerBindings;
for (FRangeLoadView Item : Src.AsRanges())
{
LoadRange(It, Item, InnerBindings);
It += Stride;
}
}
else
{
for (FStructLoadView Item : Src.AsStructs())
{
LoadStruct(It, Item);
It += Stride;
}
}
Rehash(Dst);
}
// Load leaves into non-empty set
inline void InsertNonEmpty(FScriptSet& Dst, FRangeLoadView Src) const requires(bLeaves)
{
for (LeafType Item : CastAs<LeafType>(Src.AsLeaves()))
{
if (!HasItem(Dst, &Item))
{
void* Elem = Dst.GetData(Dst.AddUninitialized(Layout), Layout);
*static_cast<LeafType*>(Elem) = Item;
}
}
Rehash(Dst);
}
inline void* AddItem(FScriptSparseArray& Dst, int32& OutIdx) const
{
OutIdx = Dst.AddUninitialized(Layout.SparseArrayLayout);
void* Out = Dst.GetData(OutIdx, Layout.SparseArrayLayout);
Inner.InitItem(Out);
return Out;
}
// Load structs or ranges into non-empty set
inline void InsertNonEmpty(FScriptSet& DstSet, FRangeLoadView Src) const
{
// Written to avoid FProperty::CopyCompleteValue_InContainer dependency
// Items are loaded directly into sparse array and then removed if a duplicate existed
FScriptSparseArray& Dst = AsSparseArray(DstSet);
const int32 OldNum = Dst.NumUnchecked();
int32 TmpIdx;
void* Tmp = AddItem(Dst, /* out */ TmpIdx);
if constexpr (ElemKind == EPropertyKind::Range)
{
TConstArrayView<FRangeBinding> InnerBindings = Range.InnerBindings;
for (FRangeLoadView Item : Src.AsRanges())
{
LoadRange(Tmp, Item, InnerBindings);
Tmp = HasItem(DstSet, Tmp) ? Tmp : AddItem(Dst, /* out */ TmpIdx);
}
}
else
{
for (FStructLoadView Item : Src.AsStructs())
{
LoadStruct(Tmp, Item);
Tmp = HasItem(DstSet, Tmp) ? Tmp : AddItem(Dst, /* out */ TmpIdx);
}
}
Inner.DestroyItem(Tmp);
Dst.RemoveAtUninitialized(Layout.SparseArrayLayout, TmpIdx, 1);
if (Dst.NumUnchecked() != OldNum)
{
Rehash(DstSet);
}
}
inline void Remove(FScriptSet& Dst, FRangeLoadView Src) const requires(bLeaves)
{
for (LeafType Item : CastAs<LeafType>(Src.AsLeaves()))
{
RemoveElem(Dst, &Item);
}
}
inline void Remove(FScriptSet& Dst, FRangeLoadView Src) const
{
TArray<uint8, TInlineAllocator<64>> Buffer;
Buffer.SetNumUninitialized(Inner.Size);
Inner.InitItem(Buffer.GetData());
void* Tmp = Buffer.GetData();
if constexpr (ElemKind == EPropertyKind::Range)
{
TConstArrayView<FRangeBinding> Inners = Range.InnerBindings;
for (FRangeLoadView Item : Src.AsRanges())
{
LoadRange(Tmp, Item, Inners);
RemoveElem(Dst, Tmp);
}
}
else
{
for (FStructLoadView Item : Src.AsStructs())
{
LoadStruct(Tmp, Item);
RemoveElem(Dst, Tmp);
}
}
Inner.DestroyItem(Tmp);
}
inline void DestroyRemoved(FScriptSet& Dst, int32 Idx) const
{
checkSlow(Idx < Dst.GetMaxIndex());
checkSlow(!Dst.IsValidIndex(Idx));
void* Elem = AsSparseArray(Dst).GetData(Idx, Layout.SparseArrayLayout);
Inner.DestroyItem(Elem);
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
class FSetBindings
{
TMap<FParameterBinding, FBindId> Bindings;
template<EPropertyKind ItemKind>
void BindNew(FSetProperty* Property, FMemberBinding Elem, FBothStructId Both)
{
// Todo: Ownership / memory leak
TSetPropertyBinding<ItemKind>* Leak = new TSetPropertyBinding<ItemKind>(Property, Elem);
GUE.Customs.BindStruct(Both.BindId, *Leak, Leak->DeclareCustom(Both.DeclId), {});
}
void DeltaBindNew(FSetProperty* Property, FMemberBinding Elem, FBothStructId Both)
{
switch (GetPropertyKind(Elem))
{
case EPropertyKind::Range: BindNew<EPropertyKind::Range >(Property, Elem, Both); break;
case EPropertyKind::Struct: BindNew<EPropertyKind::Struct>(Property, Elem, Both); break;
case EPropertyKind::Bool: BindNew<EPropertyKind::Bool >(Property, Elem, Both); break;
case EPropertyKind::U8: BindNew<EPropertyKind::U8 >(Property, Elem, Both); break;
case EPropertyKind::U16: BindNew<EPropertyKind::U16 >(Property, Elem, Both); break;
case EPropertyKind::U32: BindNew<EPropertyKind::U32 >(Property, Elem, Both); break;
case EPropertyKind::U64: BindNew<EPropertyKind::U64 >(Property, Elem, Both); break;
case EPropertyKind::F32: BindNew<EPropertyKind::F32 >(Property, Elem, Both); break;
case EPropertyKind::F64: BindNew<EPropertyKind::F64 >(Property, Elem, Both); break;
default: check(false); break;
}
}
public:
FBindId Bind(FSetProperty* Property, FMemberBinding Elem)
{
check(Elem.Offset == 0);
if (const FBindId* BindId = Bindings.Find(FParameterBinding(Elem)))
{
return *BindId;
}
// Index custom delta binding struct name
FBothType Param = Elem.IndexParameterName(GUE.Names);
FType BindType = FType{ GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Set, MakeArrayView(&Param.BindType, 1))) };
FType DeclType = Param.IsLowered()
? FType{ GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Set, MakeArrayView(&Param.DeclType, 1))) }
: BindType;
FBindId BindId = GUE.Names.IndexBindId(BindType);
FDeclId DeclId = Param.IsLowered() ? GUE.Names.IndexDeclId(DeclType) : LowerCast(BindId);
DeltaBindNew(Property, Elem, {BindId, DeclId});
return Bindings.Emplace(Elem, BindId);
}
};
static FSetBindings GSets;
////////////////////////////////////////////////////////////////////////////////////////////////
// Flat TMap binding
template<class ScriptMap, EPropertyKind KeyKind, EPropertyKind ValueKind>
struct TMapPropertyItemBinding : IItemRangeBinding
{
const FScriptMapLayout Layout;
const TInnerProperty<KeyKind> InnerKey;
const TInnerProperty<ValueKind> InnerValue;
TMapPropertyItemBinding(FMapProperty* In)
: IItemRangeBinding(GUE.Typenames.Map)
, Layout(In->MapLayout)
, InnerKey(In->KeyProp)
, InnerValue(In->ValueProp)
{}
inline SIZE_T GetStride() const { return Layout.SetLayout.Size; }
inline uint8* InitMap(ScriptMap& Map, int32 Num) const
{
uint8* It = SetNumUninitialized(Map, Layout, Num);
InitStridedItems(InnerKey, It, Num, GetStride());
InitStridedItems(InnerValue, It + Layout.ValueOffset, Num, GetStride());
return It;
}
inline void Rehash(ScriptMap& Map) const
{
Map.Rehash(Layout, MakeHashFn(InnerKey));
}
inline void DestroyAll(ScriptMap& Map) const
{
if (InnerKey.bDestruct || InnerValue.bDestruct)
{
const SIZE_T Stride = GetStride();
const int32 ValueOffset = Layout.ValueOffset;
const int32 Num = Map.NumUnchecked();
uint8* It = static_cast<uint8*>(Map.GetData(0, Layout));
if (IsCompact(Map))
{
DestroyStridedItems(InnerKey, It, Num, Stride);
DestroyStridedItems(InnerValue, It + ValueOffset, Num, Stride);
}
else
{
for (int32 Idx = 0, Max = Map.GetMaxIndex(); Idx < Max; ++Idx)
{
if (Map.IsValidIndex(Idx))
{
InnerKey.DestroyItem(It);
InnerValue.DestroyItem(It + ValueOffset);
}
It += Stride;
}
}
}
}
virtual void ReadItems(FSaveRangeContext& Ctx) const override
{
ReadSparseItems(/* out */ Ctx.Items, Ctx.Request.GetRange<FScriptSparseArray>(), Layout.SetLayout.SparseArrayLayout);
}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
ScriptMap& Map = Ctx.Request.GetRange<ScriptMap>();
int32 NewNum = static_cast<int32>(Ctx.Request.NumTotal());
if (Ctx.Request.IsFirstCall())
{
DestroyAll(Map);
Map.Empty(NewNum, Layout);
if (NewNum)
{
void* Items = InitMap(Map, NewNum);
Ctx.Items.Set(Items, Ctx.Request.NumTotal(), GetStride());
Ctx.Items.RequestFinalCall();
}
}
else
{
check(Ctx.Request.IsFinalCall());
Rehash(Map);
}
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
struct FMapMemberBindings
{
FMemberBinding Key;
FMemberBinding Value;
FMemberBinding Pair;
};
template <EPropertyKind KeyKind, EPropertyKind ValueKind>
struct TMapPropertyCustomBinding : TMapPropertyItemBinding<FScriptMap, KeyKind, ValueKind>, ICustomBinding
{
using Super = TMapPropertyItemBinding<FScriptMap, KeyKind, ValueKind>;
using Super::Layout;
using Super::InnerKey;
using Super::InnerValue;
using Super::GetStride;
using Super::InitMap;
using Super::Rehash;
using SubSetIterator = TSubSetIterator<FScriptMap, FScriptMapLayout>;
const TPropertyRangeSerializer<KeyKind> KeyRange;
const TPropertyRangeSerializer<ValueKind> ValueRange;
const FStructRangeSerializer PairRange;
const TPropertyRangeSerializer<KeyKind>& GetKeyRange() const { return KeyRange; }
const FStructRangeSerializer& GetItemRange() const { return PairRange; }
TMapPropertyCustomBinding(FMapProperty* Map, FMapMemberBindings Members)
: Super(Map)
, KeyRange(Members.Key)
, ValueRange(Members.Value)
, PairRange(Members.Pair)
{}
FStructDeclarationPtr Declare(FDeclId Id) const
{
FMemberId Members[] = { GUE.Members.Assign, GUE.Members.Remove, GUE.Members.Insert };
FMemberSpec KeysSpec = KeyRange.SpecMember();
FMemberSpec PairsSpec = PairRange.SpecMember();
return PlainProps::Declare({Id, NoId, 0, EMemberPresence::AllowSparse, Members, {PairsSpec, KeysSpec, PairsSpec}});
}
inline const void* GetValue(const void* Pair) const
{
return static_cast<const uint8*>(Pair) + Layout.ValueOffset;
}
inline int32 FindKey(const FScriptMap& Map, const void* Key) const
{
return Map.FindPairIndex(Key, Layout, MakeHashFn(InnerKey), MakeIdenticalFn(InnerKey));
}
inline bool HasKey(const FScriptMap& Map, const void* Key) const
{
return FindKey(Map, Key) != INDEX_NONE;
}
inline bool HasItem(const FScriptMap& Map, const void* Pair) const
{
const void* Key = Pair;
if (int32 Idx = FindKey(Map, Key); Idx != INDEX_NONE)
{
const void* FoundPair = Map.GetData(Idx, Layout);
return MakeIdenticalFn(InnerValue)(GetValue(Pair), GetValue(FoundPair));
}
return false;
}
inline uint8* AddPair(FScriptSparseArray& Dst, int32& OutIdx, int32 ValueOffset) const
{
OutIdx = Dst.AddUninitialized(Layout.SetLayout.SparseArrayLayout);
uint8* Out = static_cast<uint8*>(Dst.GetData(OutIdx, Layout.SetLayout.SparseArrayLayout));
InnerKey.InitItem(Out);
InnerValue.InitItem(Out + ValueOffset);
return Out;
}
inline void DestroyPair(FScriptMap& Map, int32 Idx) const
{
if (InnerKey.bDestruct || InnerValue.bDestruct)
{
void* Pair = Map.GetData(Idx, Layout);
InnerKey.DestroyItem(Pair);
InnerValue.DestroyItem(static_cast<uint8*>(Pair) + Layout.ValueOffset);
}
}
inline void RemoveKey(FScriptMap& Map, const void* Key) const
{
if (int32 Idx = FindKey(Map, Key); Idx != INDEX_NONE)
{
DestroyPair(Map, Idx);
Map.RemoveAt(Idx, Layout);
}
}
virtual void SaveCustom(FMemberBuilder& Dst, const void* Src, const void* Default, const FSaveContext& Ctx) override
{
SaveSetDelta(*this, Dst, *static_cast<const FScriptMap*>(Src), static_cast<const FScriptMap*>(Default), Ctx);
}
virtual void LoadCustom(void* Dst, FStructLoadView Src, ECustomLoadMethod Method) const override
{
check(Method == ECustomLoadMethod::Assign);
LoadSetDelta(*this, *static_cast<FScriptMap*>(Dst), Src);
}
// Load into empty set
inline void AssignEmpty(FScriptMap& Dst, FRangeLoadView Src) const
{
const int32 ValueOffset = Layout.ValueOffset;
const SIZE_T Stride = GetStride();
uint8* It = InitMap(Dst, Src.Num());
if (!Src.IsEmpty())
{
FOptionalSchemaId InnerLoadIds[2];
FStructRangeLoadView Structs = Src.AsStructs();
Structs.GetSchema().GetInnerLoadIds(/* out */ MakeArrayView(InnerLoadIds));
for (FStructLoadView Struct : Structs)
{
// Equivalent to LoadStruct(It, Struct);
FBitCacheReader Bits;
KeyRange.LoadItem(It, /* in-out */ Struct.Values, /* in-out */ Bits, InnerLoadIds[0], Struct.Schema.Batch);
ValueRange.LoadItem(It + ValueOffset, /* in-out */ Struct.Values, /* in-out */ Bits, InnerLoadIds[1], Struct.Schema.Batch);
It += Stride;
}
}
Rehash(Dst);
}
// Load structs or ranges into non-empty map
inline void InsertNonEmpty(FScriptMap& Dst, FRangeLoadView Src) const
{
// Written to avoid FProperty::CopyCompleteValue_InContainer dependency
// Items are loaded directly into sparse array and then removed if a duplicate existed
FScriptSparseArray& DstArray = AsSparseArray(Dst);
const int32 OldNum = DstArray.NumUnchecked();
const int32 ValueOffset = Layout.ValueOffset;
int32 TmpIdx;
uint8* Tmp = AddPair(DstArray, /* out*/ TmpIdx, ValueOffset);
FOptionalSchemaId InnerLoadIds[2];
FStructRangeLoadView Structs = Src.AsStructs();
Structs.GetSchema().GetInnerLoadIds(/* out */ MakeArrayView(InnerLoadIds));
for (FStructLoadView Struct : Structs)
{
// Equivalent to LoadStruct(It, Struct);
FBitCacheReader Bits;
KeyRange.LoadItem(Tmp, /* in-out */ Struct.Values, /* in-out */ Bits, InnerLoadIds[0], Struct.Schema.Batch);
if (int32 Idx = FindKey(Dst, Tmp); Idx != INDEX_NONE)
{
// Load value into existing pair
uint8* Pair = static_cast<uint8*>(DstArray.GetData(Idx, Layout.SetLayout.SparseArrayLayout));
ValueRange.LoadItem(Pair + ValueOffset, /* in-out */ Struct.Values, /* in-out */ Bits, InnerLoadIds[1], Struct.Schema.Batch);
}
else
{
// Load value into tmp pair and add new temporary
ValueRange.LoadItem(Tmp + ValueOffset, /* in-out */ Struct.Values, /* in-out */ Bits, InnerLoadIds[1], Struct.Schema.Batch);
Tmp = AddPair(DstArray, /* out*/ TmpIdx, ValueOffset);
}
}
InnerKey.DestroyItem(Tmp);
InnerValue.DestroyItem(Tmp + ValueOffset);
DstArray.RemoveAtUninitialized(Layout.SetLayout.SparseArrayLayout, TmpIdx, 1);
if (DstArray.NumUnchecked() != OldNum)
{
Rehash(Dst);
}
}
inline void Remove(FScriptMap& Dst, FRangeLoadView Src) const
{
using LeafType = EquivalentLeafType<KeyKind>;
for (LeafType Item : CastAs<LeafType>(Src.AsLeaves()))
{
RemoveKey(Dst, &Item);
}
}
inline void Remove(FScriptMap& Dst, FRangeLoadView Src) const requires (KeyKind == EPropertyKind::Range)
{
TArray<uint8, TInlineAllocator<64>> Buffer;
Buffer.SetNumUninitialized(InnerKey.Size);
void* Tmp = Buffer.GetData();
InnerKey.InitItem(Tmp);
for (FRangeLoadView Item : Src.AsRanges())
{
LoadRange(Tmp, Item, KeyRange.InnerBindings);
RemoveKey(Dst, Tmp);
}
InnerKey.DestroyItem(Tmp);
}
inline void Remove(FScriptMap& Dst, FRangeLoadView Src) const requires (KeyKind == EPropertyKind::Struct)
{
TArray<uint8, TInlineAllocator<64>> Buffer;
Buffer.SetNumUninitialized(InnerKey.Size);
void* Tmp = Buffer.GetData();
InnerKey.InitItem(Tmp);
for (FStructLoadView Item : Src.AsStructs())
{
LoadStruct(Tmp, Item);
RemoveKey(Dst, Tmp);
}
InnerKey.DestroyItem(Tmp);
}
virtual bool DiffCustom(const void* A, const void* B, const FBindContext&) const override
{
return DiffSet(*this, *static_cast<const FScriptMap*>(A), *static_cast<const FScriptMap*>(B));
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
class FMapBindings
{
TMap<FBindId, FBindId> NormalBindings;
TMap<FBindId, FRangeBinding> FrozenBindings;
template<EPropertyKind KeyKind, EPropertyKind ValueKind>
static IItemRangeBinding* New3(FMapProperty* Property, FMapMemberBindings Members, FBindId* OutCustomId)
{
if (OutCustomId == nullptr) // Freezable maps aren't delta serialized
{
return new TMapPropertyItemBinding<FFreezableScriptMap, KeyKind, ValueKind>(Property);
}
// Index custom delta binding struct name
FBothStructId Both;
if (Members.Key.RangeBindings.Num() + Members.Value.RangeBindings.Num() > 0)
{
FBothType BothKey = Members.Key.IndexParameterName(GUE.Names);
FBothType BothValue = Members.Value.IndexParameterName(GUE.Names);
checkf(BothKey.IsLowered() || BothValue.IsLowered(), TEXT("Key or Value is range-bound and should be type-erased / lowered"));
FType BindParams[2] = {BothKey.BindType, BothValue.BindType};
FType DeclParams[2] = {BothKey.DeclType, BothValue.DeclType};
FType BindType = { GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Map, BindParams)) };
FType DeclType = { GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Map, DeclParams)) };
Both = { GUE.Names.IndexBindId(BindType), GUE.Names.IndexDeclId(DeclType) };
}
else
{
checkf(!Members.Key.IndexParameterName(GUE.Names).IsLowered(), TEXT("Only range-bound keys or values should be type-erased / lowered"));
FParametricTypeId PairTypename = GUE.Names.Resolve(Members.Pair.InnermostSchema.Get().AsStruct()).Name.AsParametric();
TConstArrayView<FType> Params = GUE.Names.Resolve(PairTypename).GetParameters();
FType Type = { GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Map, Params)) };
FDualStructId Dual{GUE.Names.IndexStruct(Type)};
Both = { Dual, Dual };
}
// Todo: Ownership / memory leak
auto Out = new TMapPropertyCustomBinding<KeyKind, ValueKind>(Property, Members);
GUE.Customs.BindStruct(Both.BindId, *Out, Out->Declare(Both.DeclId), {});
*OutCustomId = Both.BindId;
return Out;
}
template<EPropertyKind KeyKind>
inline IItemRangeBinding* New2(FMapProperty* Property, FMapMemberBindings Members, FBindId* OutCustomId)
{
switch (GetPropertyKind(Members.Value))
{
case EPropertyKind::Range: return New3<KeyKind, EPropertyKind::Range>(Property, Members, OutCustomId);
case EPropertyKind::Struct: return New3<KeyKind, EPropertyKind::Struct>(Property, Members, OutCustomId);
case EPropertyKind::Bool: return New3<KeyKind, EPropertyKind::Bool>(Property, Members, OutCustomId);
case EPropertyKind::U8: return New3<KeyKind, EPropertyKind::U8>(Property, Members, OutCustomId);
case EPropertyKind::U16: return New3<KeyKind, EPropertyKind::U16>(Property, Members, OutCustomId);
case EPropertyKind::U32: return New3<KeyKind, EPropertyKind::U32>(Property, Members, OutCustomId);
case EPropertyKind::U64: return New3<KeyKind, EPropertyKind::U64>(Property, Members, OutCustomId);
case EPropertyKind::F32: return New3<KeyKind, EPropertyKind::F32>(Property, Members, OutCustomId);
case EPropertyKind::F64: return New3<KeyKind, EPropertyKind::F64>(Property, Members, OutCustomId);
default: check(false); return nullptr;
}
}
inline IItemRangeBinding* New(FMapProperty* Property, FMapMemberBindings Members, FBindId* OutCustomId)
{
switch (GetPropertyKind(Members.Key))
{
case EPropertyKind::Range: return New2<EPropertyKind::Range>(Property, Members, OutCustomId);
case EPropertyKind::Struct: return New2<EPropertyKind::Struct>(Property, Members, OutCustomId);
case EPropertyKind::Bool: return New2<EPropertyKind::Bool>(Property, Members, OutCustomId);
case EPropertyKind::U8: return New2<EPropertyKind::U8>(Property, Members, OutCustomId);
case EPropertyKind::U16: return New2<EPropertyKind::U16>(Property, Members, OutCustomId);
case EPropertyKind::U32: return New2<EPropertyKind::U32>(Property, Members, OutCustomId);
case EPropertyKind::U64: return New2<EPropertyKind::U64>(Property, Members, OutCustomId);
case EPropertyKind::F32: return New2<EPropertyKind::F32>(Property, Members, OutCustomId);
case EPropertyKind::F64: return New2<EPropertyKind::F64>(Property, Members, OutCustomId);
default: check(false); return nullptr;
}
}
public:
FBindId BindNormal(FMapProperty* Property, FBindId PairId, FMapMemberBindings Members)
{
if (const FBindId* CustomId = NormalBindings.Find(PairId))
{
return *CustomId;
}
FBindId CustomId;
New(Property, Members, /* out */ &CustomId);
return NormalBindings.Emplace(PairId, CustomId);
}
FRangeBinding BindFreezable(FMapProperty* Property, FBindId PairId, FMapMemberBindings Members)
{
if (const FRangeBinding* RangeBinding = FrozenBindings.Find(PairId))
{
return *RangeBinding;
}
IItemRangeBinding* Leak = New(Property, Members, nullptr);
return FrozenBindings.Emplace(PairId, FRangeBinding(*Leak, DefaultRangeMax));
}
};
static FMapBindings GMaps;
////////////////////////////////////////////////////////////////////////////////////////////////
// @pre Binding.InnermostSchema isn't lowered
static FMemberSpec ToSpec(FMemberBinding Binding)
{
FMemberSpec Out = Binding.InnermostType.IsLeaf()
? FMemberSpec(ToLeafType(Binding.InnermostType.AsLeaf()), ToOptionalEnum(Binding.InnermostSchema))
: FMemberSpec(Binding.InnermostSchema.Get().AsStructDeclId());
for (FRangeBinding RangeBinding : Binding.RangeBindings)
{
Out.RangeWrap(RangeBinding.GetSizeType());
}
return Out;
}
class FPairBindings
{
struct FPair
{
FMemberBinding KV[2];
friend uint32 GetTypeHash(FPair In) { return HashCombineFast(HashSkipOffset(In.KV[0]), HashSkipOffset(In.KV[1])); };
inline bool operator==(const FPair& O) const { return EqSkipOffset(KV[0], O.KV[0]) && EqSkipOffset(KV[1], O.KV[1]); }
};
TMap<FPair, FBindId> Bindings;
inline FBindId BindImpl(FPair Pair)
{
check(Pair.KV[0].Offset == 0 && Pair.KV[1].Offset > 0);
if (const FBindId* BindId = Bindings.Find(Pair))
{
return *BindId;
}
// Index names, can be optimized by checking if KeyParam / BindParam IsLowered()
// or better checking if either one of them is a range
FBothType KeyParam = Pair.KV[0].IndexParameterName(GUE.Names);
FBothType ValueParam = Pair.KV[1].IndexParameterName(GUE.Names);
FType BindParams[2] = { KeyParam.BindType, ValueParam.BindType };
FType DeclParams[2] = { KeyParam.DeclType, ValueParam.DeclType };
FType BindType = { GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Pair, BindParams)) };
FType DeclType = { GUE.Scopes.Core, FTypenameId(GUE.Names.MakeParametricTypeId(GUE.Typenames.Pair, DeclParams)) };
FBindId BindId = GUE.Names.IndexBindId(BindType);
FDeclId DeclId = GUE.Names.IndexDeclId(DeclType);
FMemberId Members[2] = { GUE.Members.Key, GUE.Members.Value };
FMemberSpec Specs[2] = { ToSpec(Pair.KV[0]), ToSpec(Pair.KV[1]) };
// Todo: Ownership / memory leak
GUE.Schemas.BindStruct(BindId, Pair.KV, {DeclId, NoId, 0, EMemberPresence::RequireAll, Members, {Specs}});
Bindings.Emplace(Pair, BindId);
return BindId;
}
public:
inline FMemberBinding Bind(FMemberBinding Key, FMemberBinding Value)
{
FMemberBinding Out(0);
Out.InnermostSchema = FInnerId(BindImpl(FPair{Key, Value}));
Out.InnermostType = DefaultStructBindType;
return Out;
}
};
static FPairBindings GPairs;
//////////////////////////////////////////////////////////////////////////////////////////////
inline void ReadBoolOptionalItem(FSaveRangeContext& Ctx, uint32 ItemSize)
{
checkf((&Ctx.Request.GetRange<uint8>())[ItemSize] <= uint8(true),
TEXT("Non-intrusive TOptional::bIsSet should be true or false, but byte at offset %d was %d"), ItemSize, (&Ctx.Request.GetRange<uint8>())[ItemSize]);
bool bSet = (&Ctx.Request.GetRange<bool>())[ItemSize];
Ctx.Items.SetAll(bSet ? Ctx.Request.Range : nullptr, uint64(bSet), ItemSize);
}
inline void MakeBoolOptionalItem(FLoadRangeContext& Ctx, uint32 ItemSize)
{
check((&Ctx.Request.GetRange<uint8>())[ItemSize] <= 1);
bool& bSet = (&Ctx.Request.GetRange<bool>())[ItemSize];
bSet = Ctx.Request.NumTotal() > 0;
Ctx.Items.Set(&Ctx.Request.GetRange<uint8>(), uint64(bSet), ItemSize);
}
static constexpr std::string_view TrivialOptionalName = "TrivialOptional";
template<uint32 ItemSize>
struct TTrivialOptionalBinding : IItemRangeBinding
{
TTrivialOptionalBinding() : IItemRangeBinding(GUE.Names.IndexRangeBindName(ToAnsiView(Concat<TrivialOptionalName, HexString<ItemSize>>))) {}
virtual void ReadItems(FSaveRangeContext& Ctx) const override { ReadBoolOptionalItem(Ctx, ItemSize); }
virtual void MakeItems(FLoadRangeContext& Ctx) const override { MakeBoolOptionalItem(Ctx, ItemSize); }
};
struct FTrivialOptionalBinding : IItemRangeBinding
{
const uint32 ItemSize;
explicit FTrivialOptionalBinding(uint32 Size) : IItemRangeBinding(GUE.Typenames.TrivialOptional), ItemSize(Size) {}
virtual void ReadItems(FSaveRangeContext& Ctx) const override { ReadBoolOptionalItem(Ctx, ItemSize); }
virtual void MakeItems(FLoadRangeContext& Ctx) const override { MakeBoolOptionalItem(Ctx, ItemSize); }
};
struct FOptionalBindingBase : IItemRangeBinding
{
const FProperty* Inner;
const uint32 ItemSize;
const bool bConstructor;
const bool bDestructor;
explicit FOptionalBindingBase(const FProperty* In, FConcreteTypenameId BindName)
: IItemRangeBinding(BindName)
, Inner(In)
, ItemSize(In->GetElementSize())
, bConstructor(HasConstructor(In))
, bDestructor(HasDestructor(In))
{}
void InitItem(void* Value) const
{
if (bConstructor)
{
ConstructValue(Inner, Value);
}
else
{
FMemory::Memzero(Value, ItemSize);
}
}
};
struct FIntrusiveOptionalBinding : FOptionalBindingBase
{
explicit FIntrusiveOptionalBinding(const FProperty* In)
: FOptionalBindingBase(In, GUE.Typenames.IntrusiveOptional)
{}
virtual void ReadItems(FSaveRangeContext& Ctx) const override
{
bool bSet = Inner->IsIntrusiveOptionalValueSet(Ctx.Request.Range);
Ctx.Items.SetAll(bSet ? Ctx.Request.Range : nullptr, uint64(bSet), ItemSize);
}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
uint8* Value = &Ctx.Request.GetRange<uint8>();
Inner->ClearIntrusiveOptionalValue(Value);
if (Ctx.Request.NumTotal() > 0)
{
InitItem(Value);
Ctx.Items.Set(Value, 1, ItemSize);
}
else
{
Ctx.Items.Set(nullptr, 0, ItemSize);
}
}
};
struct FNonIntrusiveOptionalBinding : FOptionalBindingBase
{
explicit FNonIntrusiveOptionalBinding(const FProperty* In)
: FOptionalBindingBase(In, GUE.Typenames.NonIntrusiveOptional)
{}
virtual void ReadItems(FSaveRangeContext& Ctx) const override
{
ReadBoolOptionalItem(Ctx, ItemSize);
}
virtual void MakeItems(FLoadRangeContext& Ctx) const override
{
uint8* Value = &Ctx.Request.GetRange<uint8>();
bool& bSet = reinterpret_cast<bool&>(Value[ItemSize]);
if (bDestructor && bSet)
{
DestroyValue(Inner, Value);
}
bSet = Ctx.Request.NumTotal() > 0;
Ctx.Items.Set(bSet ? Value : nullptr , 1, ItemSize);
if (bSet)
{
InitItem(Value);
}
}
};
class FOptionalBindings
{
TTrivialOptionalBinding<1> Trivial1;
TTrivialOptionalBinding<2> Trivial2;
TTrivialOptionalBinding<4> Trivial4;
TTrivialOptionalBinding<8> Trivial8;
TTrivialOptionalBinding<12> Trivial12;
TTrivialOptionalBinding<16> Trivial16;
TTrivialOptionalBinding<24> Trivial24;
TTrivialOptionalBinding<32> Trivial32;
TMap<FParameterBinding, FRangeBinding> NormalBindings;
TMap<FParameterBinding, FRangeBinding> IntrusiveBindings;
const IItemRangeBinding* BindNew(FProperty* Inner)
{
if (HasConstructor(Inner) || HasDestructor(Inner))
{
return new FNonIntrusiveOptionalBinding(Inner);
}
else switch (Inner->GetElementSize())
{
case 1: return &Trivial1;
case 2: return &Trivial2;
case 4: return &Trivial4;
case 8: return &Trivial8;
case 12: return &Trivial12;
case 16: return &Trivial16;
case 24: return &Trivial24;
case 32: return &Trivial32;
default: return new FTrivialOptionalBinding(Inner->GetElementSize());
}
}
public:
FRangeBinding Bind(FProperty* Inner, FMemberBinding Key)
{
check(Key.Offset == 0);
bool bIntrusive = Inner->HasIntrusiveUnsetOptionalState();
TMap<FParameterBinding, FRangeBinding>& Bindings = bIntrusive ? IntrusiveBindings : NormalBindings;
if (const FRangeBinding* Binding = Bindings.Find(FParameterBinding(Key)))
{
return *Binding;
}
// Todo: Ownership / memory leak
const IItemRangeBinding* Out = bIntrusive ? new FIntrusiveOptionalBinding(Inner) : BindNew(Inner);
return Bindings.Emplace(Key, FRangeBinding(*Out, ERangeSizeType::Uni));
}
};
static FOptionalBindings GOptionals;
//////////////////////////////////////////////////////////////////////////////////////////////
struct FStringBindings
{
TStringBinding<FString> TCharInstance{GUE.Typenames.String};
TStringBinding<FUtf8String> Utf8Instance{GUE.Typenames.Utf8String};
TStringBinding<FAnsiString> AnsiInstance{GUE.Typenames.AnsiString};
TStringBinding<FUtf8String> VerseInstance{GUE.Typenames.VerseString}; // Bypass Verse::FNativeString for now
FRangeBinding TChar{TCharInstance, DefaultRangeMax};
FRangeBinding Utf8{Utf8Instance, DefaultRangeMax};
FRangeBinding Ansi{AnsiInstance, DefaultRangeMax};
FRangeBinding Verse{VerseInstance, DefaultRangeMax};
inline const FRangeBinding& SelectBinding(uint64 CastFlags) const
{
switch (CastFlags & StringMask)
{
case CASTCLASS_FStrProperty: return TChar;
case CASTCLASS_FUtf8StrProperty: return Utf8;
case CASTCLASS_FAnsiStrProperty: return Ansi;
case CASTCLASS_FVerseStringProperty: return Verse;
default: break;
}
check(FMath::CountBits(CastFlags & StringMask) == 1);
check(false);
return TChar;
}
FMemberBinding Bind(FProperty* Property, uint64 CastFlags) const
{
const FRangeBinding& Binding = SelectBinding(CastFlags);
FMemberBinding Out(Property->GetOffset_ForInternal());
Out.InnermostType = FMemberBindType(ReflectLeaf<char8_t>);
Out.RangeBindings = MakeArrayView(&Binding, 1);
return Out;
}
};
static const FStringBindings GStrings; // static init dependency after GUE
////////////////////////////////////////////////////////////////////////////////////////////////
struct FStaticArrayBinding : IItemRangeBinding
{
uint32 Num;
uint32 Stride;
FStaticArrayBinding(uint32 InNum, uint32 InStride)
: IItemRangeBinding(GUE.Typenames.StaticArray)
, Num(InNum)
, Stride(InStride)
{}
virtual void ReadItems(FSaveRangeContext& Ctx) const override { Ctx.Items.SetAll(Ctx.Request.Range, Num, Stride); }
virtual void MakeItems(FLoadRangeContext& Ctx) const override { Ctx.Items.Set(&Ctx.Request.GetRange<uint8>(), Num, Stride); }
};
////////////////////////////////////////////////////////////////////////////////////////////////
// Serialize property-less UObject and UScriptStructs as their super class.
//
// E.g. FVector_NetQuantize10 is a pure runtime abstraction serialized as FVector.
// FAttenuationSubmixSendSettings is just a FSoundSubmixSendInfoBase but has a different
// default constructor that matters in sparse delta serialization.
// UObjects are never instantiated during serialization so can be safely simplified.
//
// These heuristics might need more tuning
static const UStruct* SkipEmptyBases(const UStruct* In)
{
const UStruct* FirstOwner = In->PropertyLink ? In->PropertyLink->GetOwnerChecked<UStruct>() : In;
if (In != FirstOwner)
{
if (const UScriptStruct* Struct = Cast<const UScriptStruct>(In))
{
if (!(Struct->StructFlags & STRUCT_ZeroConstructor))
{
return In;
}
}
return FirstOwner;
}
return In;
}
////////////////////////////////////////////////////////////////////////////////////////////////
class FPropertyBinder
{
public:
FPropertyBinder(FBindId Id, EMemberPresence InOccupancy)
: Owner(Id)
, Occupancy(InOccupancy)
{}
void BindSuper(FDeclId SuperId);
void BindMember(FProperty* Property);
TConstArrayView<FMemberBinding> GetMembers() const { return Members; }
FStructDeclarationPtr Declare() const;
// Only true for noexport UScriptStruct with STRUCT_Immutable | STRUCT_Atomic flags
bool IsDense() { return Occupancy == EMemberPresence::RequireAll; }
private:
const FBindId Owner;
FOptionalDeclId Super;
TOptional<FScopeId> OwnerScope;
const EMemberPresence Occupancy;
TPagedArray<FRangeBinding, 1024> Ranges;
TArray<FMemberId, TInlineAllocator<64>> Names;
TArray<FMemberBinding, TInlineAllocator<64>> Members;
TArray<FMemberSpec, TInlineAllocator<64>> Specs;
// BPVM only?
const FName VerseFunctionProperty{"VerseFunctionProperty"};
const FName VerseDynamicProperty{"VerseDynamicProperty"};
const FName ReferenceProperty{"ReferenceProperty"}; // Verse reference + FProperty*
TConstArrayView<FRangeBinding> AllocateRangeBindings(FRangeBinding Head, TConstArrayView<FRangeBinding> Tail)
{
if (Tail.Num() == 0)
{
return MakeArrayView(&Ranges.Add_GetRef(Head), 1);
}
// Ensure contiguous out range by padding up with dummy tail slice
static constexpr uint32 PageMax = decltype(Ranges)::MaxPerPage();
const int32 OutNum = 1 + Tail.Num();
check(OutNum <= PageMax);
const int32 NewPages = Align(Ranges.Num() + OutNum, PageMax) / PageMax;
if (NewPages > Ranges.NumPages() && !Ranges.IsEmpty())
{
int32 NumPad = Align(Ranges.Num(), PageMax) - Ranges.Num();
Ranges.Append(Tail.Slice(0, NumPad));
check(Ranges.Num() % PageMax == 0);
}
const FRangeBinding* OutData = &Ranges.Add_GetRef(Head);
Ranges.Append(Tail);
return MakeArrayView(OutData, OutNum);
}
static FMemberBinding Todo(FProperty* Property)
{
return FMemberBinding(Property->GetOffset_ForInternal());
}
FScopeId GetOwnerScope()
{
if (!OwnerScope)
{
FType OwnerType = GUE.Names.Resolve(Owner);
OwnerScope = GUE.Names.NestFlatScope(OwnerType.Scope, {OwnerType.Name.AsConcrete().Id});
}
return OwnerScope.GetValue();
}
inline FMemberBinding BindAsRange(FProperty* Property, FRangeBinding RangeBinding, FMemberBinding Inner)
{
if (Inner.InnermostType.IsLeaf() && Inner.InnermostType.AsLeaf().Bind.Type == ELeafBindType::BitfieldBool)
{
UE_LOGFMT(LogPlainPropsUObject, Warning, "Property '{Property}' is a '{Container}' of bitfield bools, which make no sense. Binding as range of bools.",
Property->GetFName(), GUE.Debug.Print(RangeBinding.GetBindName()));
Inner.InnermostType = FMemberBindType(ReflectArithmetic<bool>);
}
FMemberBinding Out(Property->GetOffset_ForInternal());
Out.InnermostType = Inner.InnermostType;
Out.InnermostSchema = Inner.InnermostSchema;
Out.RangeBindings = AllocateRangeBindings(RangeBinding, Inner.RangeBindings);
return Out;
}
inline FMemberBinding BindAsStruct(FProperty* Property, FBindId Id)
{
FMemberBinding Out(Property->GetOffset_ForInternal());
Out.InnermostSchema = FInnerId(Id);
Out.InnermostType = DefaultStructBindType;
return Out;
}
inline FMemberBinding BindAsStruct(FProperty* Property, UStruct* Struct)
{
FType Type = IndexType(SkipEmptyBases(Struct));
return BindAsStruct(Property, GUE.Names.IndexBindId(Type));
}
inline static FBitfieldBoolBindType MakeBitfieldBool(uint8 BitIdx)
{
return {EMemberKind::Leaf, ELeafBindType::BitfieldBool, BitIdx};
}
inline FMemberBinding BindBool(FBoolProperty* Property)
{
check(Property->GetByteOffset() == 0);
FMemberBinding Out(Property->GetOffset_ForInternal());
uint8 BitIdx = static_cast<uint8>(FMath::FloorLog2NonZero(Property->GetFieldMask()));
FLeafBindType Type = Property->IsNativeBool() ? FLeafBindType(ELeafBindType::Bool, ELeafWidth::B8)
: FLeafBindType(MakeBitfieldBool(BitIdx));
Out.InnermostType = FMemberBindType(Type);
return Out;
}
inline FMemberBinding BindEnum(FEnumProperty* Property)
{
FMemberBinding Out(Property->GetOffset_ForInternal());
Out.InnermostSchema = FInnerId(GUE.Names.IndexEnum(IndexType(Property->GetEnum())));
FUnpackedLeafType Leaf = {ELeafType::Enum, WidthOf(Property->GetElementSize())};
Out.InnermostType = FMemberBindType(Leaf);
return Out;
}
inline FLeafBindType BindByte(FByteProperty* Property, FOptionalInnerId& OutEnumId)
{
if (const UEnum* Enum = Property->GetIntPropertyEnum())
{
OutEnumId = FInnerId(GUE.Names.IndexEnum(IndexType(Enum)));
return FLeafBindType(ELeafBindType::Enum, ELeafWidth::B8);
}
return FLeafBindType(ELeafBindType::IntU, ELeafWidth::B8);
}
inline FMemberBinding BindNumeric(FNumericProperty* Property, uint64 Flags)
{
FMemberBinding Out(Property->GetOffset_ForInternal());
bool bFloat = HasAny<CASTCLASS_FFloatProperty | CASTCLASS_FDoubleProperty>(Flags);
bool bIntS = HasAny<CASTCLASS_FInt8Property | CASTCLASS_FInt16Property | CASTCLASS_FIntProperty | CASTCLASS_FInt64Property>(Flags);
if (HasAny<CASTCLASS_FByteProperty>(Flags))
{
FLeafBindType Leaf = BindByte(static_cast<FByteProperty*>(Property), /* out enum */ Out.InnermostSchema);
Out.InnermostType = FMemberBindType(Leaf);
}
else
{
ELeafType Type = bFloat ? ELeafType::Float : (bIntS ? ELeafType::IntS : ELeafType::IntU);
FUnpackedLeafType Leaf = {Type, WidthOf(Property->GetElementSize())};
Out.InnermostType = FMemberBindType(Leaf);
}
return Out;
}
inline FMemberBinding BindArray(FArrayProperty* Property)
{
return BindAsRange(Property, AllocateArrayBinding(Property), BindSingleProperty(Property->Inner));
}
inline FMemberBinding BindMap(FMapProperty* Property)
{
FMemberBinding Key = BindSingleProperty(Property->KeyProp);
FMemberBinding Value = BindSingleProperty(Property->ValueProp);
FMemberBinding Pair = GPairs.Bind(Key, Value);
FBindId PairId = Pair.InnermostSchema.Get().AsStructBindId();
bool bFreezable = EnumHasAnyFlags(Property->MapFlags, EMapPropertyFlags::UsesMemoryImageAllocator);
return bFreezable ? BindAsRange(Property, GMaps.BindFreezable(Property, PairId, {Key, Value, Pair}), Pair)
: BindAsStruct(Property, GMaps.BindNormal(Property, PairId, {Key, Value, Pair}));
}
inline FMemberBinding BindSet(FSetProperty* Property)
{
FMemberBinding Elem = BindSingleProperty(Property->ElementProp);
return BindAsStruct(Property, GSets.Bind(Property, Elem));
}
inline FMemberBinding BindOptional(FOptionalProperty* Property)
{
FMemberBinding Inner = BindSingleProperty(Property->GetValueProperty());
return BindAsRange(Property, GOptionals.Bind(Property->GetValueProperty(), Inner), Inner);
}
#if WITH_VERSE_VM
inline FMemberBinding BindVValue(FVValueProperty* Property) { return Todo(Property); }
inline FMemberBinding BindVRestValue(FVRestValueProperty* Property) { return Todo(Property); }
#endif
FMemberBinding BindSingleProperty(FProperty* Property)
{
FName PropertyTypename = Property->GetClass()->GetFName();
uint64 Flags = Property->GetCastFlags();
if (HasAny<LeafMask>(Flags))
{
if (HasAny<CASTCLASS_FNumericProperty>(Flags))
{
return BindNumeric(static_cast<FNumericProperty*>(Property), Flags);
}
return HasAny<CASTCLASS_FEnumProperty>(Flags)
? BindEnum(static_cast<FEnumProperty*>(Property))
: BindBool(static_cast<FBoolProperty*>(Property));
}
else if (HasAny<CommonStructMask>(Flags))
{
return BindAsStruct(Property, FlagsToCommonBindId(Flags & CommonStructMask));
}
else if (HasAny<CASTCLASS_FStructProperty>(Flags))
{
return BindAsStruct(Property, static_cast<FStructProperty*>(Property)->Struct);
}
else if (HasAny<ContainerMask>(Flags))
{
if (HasAny<CASTCLASS_FArrayProperty>(Flags))
{
return BindArray(static_cast<FArrayProperty*>(Property));
}
if (HasAny<CASTCLASS_FMapProperty>(Flags))
{
return BindMap(static_cast<FMapProperty*>(Property));
}
return HasAny<CASTCLASS_FSetProperty>(Flags)
? BindSet(static_cast<FSetProperty*>(Property))
: BindOptional(static_cast<FOptionalProperty*>(Property));
}
else if (HasAny<StringMask>(Flags))
{
return GStrings.Bind(Property, Flags);
}
else if (HasAny<MiscMask>(Flags))
{
FBindId BindId = HasAny<CASTCLASS_FInterfaceProperty>(Flags)
? BindInterface(static_cast<FInterfaceProperty*>(Property))
: BindSparseDelegate(Owner, static_cast<FMulticastSparseDelegateProperty*>(Property));
return BindAsStruct(Property, BindId);
}
#if WITH_VERSE_VM
else if (HasAny<CASTCLASS_FVValueProperty | CASTCLASS_FVRestValueProperty>(Flags))
{
return HasAny<CASTCLASS_FVValueProperty>(Flags)
? BindVValue(static_cast<FVValueProperty*>(Property))
: BindVRestValue(static_cast<FVRestValueProperty*>(Property));
}
#else // Verse BPVM
else
{
if (PropertyTypename == VerseFunctionProperty) // FVerseFunctionProperty
{
return BindAsStruct(Property, GUE.Structs.VerseFunction);
}
else if (PropertyTypename == VerseDynamicProperty) // FVerseDynamicProperty
{
return BindAsStruct(Property, GUE.Structs.DynamicallyTypedValue);
}
else if (PropertyTypename == ReferenceProperty) // FReferenceProperty
{
return BindAsStruct(Property, GUE.Structs.ReferencePropertyValue);
}
}
#endif
checkf(false, TEXT("Unrecognized class cast flags %llx in %s %s"), Flags, *PropertyTypename.ToString(), *Property->GetNameCPP());
return FMemberBinding(Property->GetOffset_ForInternal());
}
FType MakeStaticArrayTypename(FName PropertyName)
{
return { GetOwnerScope(), GUE.Names.MakeTypename(PropertyName) };
}
FMemberBinding BindProperty(FProperty* Property)
{
FMemberBinding Out = BindSingleProperty(Property);
if (Property->GetSize() == Property->GetElementSize())
{
return Out;
}
// Bind static array
uint32 TotalSize = static_cast<uint32>(Property->GetSize());
uint32 ElementSize = static_cast<uint32>(Property->GetElementSize());
uint32 ArrayDim = TotalSize / ElementSize;
check(ArrayDim * ElementSize == TotalSize);
if (Occupancy == EMemberPresence::RequireAll || ArrayDim > FStructDeclaration::MaxMembers)
{
// Create range binding that isn't delta-serializable
//
// Could generate nested numeral structs instead. Unsure if automatic
// per-element delta serialization for massive arrays is desirable.
//
// To delta-serialize massive arrays, custom-bind the owning struct
// and implement delta serialization manually
// Todo: Ownership / memory leak
const FStaticArrayBinding& ItemBinding = *new FStaticArrayBinding(ArrayDim, ElementSize);
ERangeSizeType SizeType = ArrayDim < 256 ? ERangeSizeType::U8 : ((ArrayDim < 65536) ? ERangeSizeType::U16 : ERangeSizeType::U32);
Out.RangeBindings = AllocateRangeBindings(FRangeBinding(ItemBinding, SizeType), Out.RangeBindings);
}
else
{
// Create struct binding to allow delta serialization
FType StaticArrayType = MakeStaticArrayTypename(Property->GetFName());
FDualStructId StaticArrayId{GUE.Names.IndexStruct(StaticArrayType)};
TArray<FMemberBinding, TInlineAllocator<64>> ElemBindings;
ElemBindings.Init(Out, ArrayDim);
uint64 Offset = 0;
for (FMemberBinding& Element : ElemBindings)
{
Element.Offset = Offset;
Offset += ElementSize;
}
TConstArrayView<FMemberId> Numerals = GUE.Numerals.MakeRange(IntCastChecked<uint16>(ArrayDim));
TArray<FMemberSpec, TInlineAllocator<64>> ElemSpecs;
ElemSpecs.Init(ToSpec(Out), ArrayDim);
FStructSpec Spec = {StaticArrayId, NoId, 0, EMemberPresence::AllowSparse, Numerals, { ElemSpecs }};
// Todo: Ownership
GUE.Schemas.BindStruct(StaticArrayId, ElemBindings, Spec);
Out.InnermostType = DefaultStructBindType;
Out.InnermostSchema = FInnerId(StaticArrayId);
Out.RangeBindings = {};
}
return Out;
}
};
void FPropertyBinder::BindSuper(FDeclId SuperId)
{
check(!IsDense());
Super = SuperId;
FMemberBinding Member;
Member.InnermostType = SuperStructBindType;
Member.InnermostSchema = FInnerId(SuperId);
Members.Emplace(Member);
}
void FPropertyBinder::BindMember(FProperty* Property)
{
Members.Emplace(BindProperty(Property));
Specs.Emplace(ToSpec(Members.Last()));
Names.Emplace(GUE.Names.NameMember(Property->GetFName()));
}
FStructDeclarationPtr FPropertyBinder::Declare() const
{
return PlainProps::Declare({LowerCast(Owner), Super, 0, Occupancy, Names, {Specs}});
}
////////////////////////////////////////////////////////////////////////////////////////////////
static bool ShouldBind(const UStruct* Struct);
inline bool ShouldBind(const FProperty* Property)
{
if (HasAny<CASTCLASS_FStructProperty>(Property->GetCastFlags()))
{
return ShouldBind(static_cast<const FStructProperty*>(Property)->Struct);
}
return true;
}
static bool ShouldBind(const UStruct* Struct)
{
for (FProperty* Property = Struct->PropertyLink; Property; Property = Property->PropertyLinkNext)
{
if (ShouldBind(Property))
{
return true;
}
}
return false;
}
inline void BindMembers(FPropertyBinder& Out, const UStruct* Struct)
{
for (FProperty* It = Struct->PropertyLink; It && It->GetOwner<UStruct>() == Struct; It = It->PropertyLinkNext)
{
if (ShouldBind(It))
{
Out.BindMember(It);
}
}
}
void BindSuperMembers(FPropertyBinder& Out, const UStruct* Struct)
{
if (const UStruct* Super = Struct->GetInheritanceSuper())
{
BindSuperMembers(Out, Super);
if (ShouldBind(Super))
{
BindMembers(Out, Super);
}
}
}
const UStruct* GetSuperToBind(const UStruct* Struct)
{
const UStruct* Super = Struct->GetInheritanceSuper();
return Super && ShouldBind(Super) ? SkipEmptyBases(Super) : nullptr;
}
void BindStruct(FBindId Id, const UStruct* Struct)
{
if (GUE.Customs.FindStruct(Id))
{
return;
}
FPropertyBinder Binder(Id, GetOccupancy(Struct));
if (const UStruct* Super = GetSuperToBind(Struct))
{
if (Binder.IsDense())
{
// Flatten inheritance chain for dense structs
for (; Super; Super = GetSuperToBind(Super))
{
BindMembers(/* out */ Binder, Super);
}
}
else
{
FDeclId SuperId = GUE.Names.IndexDeclId(IndexType(Super));
Binder.BindSuper(SuperId);
}
}
BindMembers(/* out */ Binder, Struct);
GUE.Schemas.BindStruct(Id, Binder.GetMembers(), Binder.Declare());
// Don't bind CDO defaults, object defaults are passed in from top and objects aren't owned by containers
if (Struct->HasAnyCastFlags(CASTCLASS_UScriptStruct))
{
GUE.Defaults.Bind(Id, static_cast<const UScriptStruct*>(Struct));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
static void BindInitialTypes()
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Binding types to PlainProps schemas...");
// Declare all UEnums
for (TObjectIterator<UEnum> It; It; ++It)
{
DeclareEnum(*It);
}
// Bind all UScriptStruct/UClass/UFunction
static const FBindId SkipStructs[] = { GUE.Structs.VerseFunction };
for (TObjectIterator<UStruct> It; It; ++It)
{
const UStruct* Struct = *It;
FBindId Id = GUE.Names.IndexBindId(IndexType(Struct));
if (!Algo::Find(SkipStructs, Id))
{
BindStruct(Id, Struct);
}
}
}
static void InitBatchedProperties()
{
GUE.Defaults.BindZeroes(GUE.Structs.Name, sizeof(FName), alignof(FName));
GUE.Defaults.BindStatic(GUE.Structs.Text, &FText::GetEmpty());
GUE.Defaults.BindZeroes(GUE.Structs.ClassPtr, sizeof(TSubclassOf<UClass>), alignof(TSubclassOf<UClass>));
GUE.Defaults.BindZeroes(GUE.Structs.ObjectPtr, sizeof(FObjectPtr), alignof(FObjectPtr));
GUE.Defaults.BindZeroes(GUE.Structs.WeakObjectPtr, sizeof(FWeakObjectPtr), alignof(FWeakObjectPtr));
GUE.Defaults.BindZeroes(GUE.Structs.SoftObjectPtr, sizeof(FSoftObjectPtr), alignof(FSoftObjectPtr));
GUE.Defaults.BindZeroes(GUE.Structs.LazyObjectPtr, sizeof(FLazyObjectPtr), alignof(FLazyObjectPtr));
}
struct FMemoryPropertyBatch
{
TArray<FText> Texts; // Tricky to serialize intrusively
static FSoleMemberSpec Spec(FName*, FDeclId Id)
{
return {Id, GUE.Members.Id, Specify<uint64>()};
}
static void Save(FMemberBuilder& Out, FName In, const FSaveContext&)
{
Out.Add(GUE.Members.Id, FSensitiveName(In).ToUnstableInt());
}
static void Load(FName& Out, FStructLoadView In)
{
Out = FSensitiveName::FromUnstableInt(LoadSole<FSensitiveName::IntType>(In)).ToName();
}
static FSoleMemberSpec Spec(FText*, FDeclId Id)
{
return {Id, GUE.Members.Id, Specify<int32>()};
}
void Save(FMemberBuilder& Out, const FText& In, const FSaveContext&)
{
Out.Add(GUE.Members.Id, Texts.Num());
Texts.Add(In);
}
void Load(FText& Out, FStructLoadView In) const
{
Out = Texts[LoadSole<int32>(In)];
}
static FSoleMemberSpec Spec(FObjectHandle*, FDeclId Id)
{
return {Id, GUE.Members.Id, Specify<uint64>()};
}
static void Save(FMemberBuilder& Out, FObjectHandle In, const FSaveContext&)
{
static_assert(sizeof(In) == sizeof(uint64));
Out.Add(GUE.Members.Id, reinterpret_cast<const uint64&>(In));
}
static void Load(FObjectHandle& Out, FStructLoadView In)
{
LoadSole<uint64>(&Out, In);
}
static FSoleMemberSpec Spec(FWeakObjectPtr*, FDeclId Id)
{
return {Id, GUE.Members.Id, Specify<uint64>()};
}
static void Save(FMemberBuilder& Out, const FWeakObjectPtr& In, const FSaveContext&)
{
// Save ObjectSerialNumber + ObjectIndex a single uint64
static_assert(sizeof(FWeakObjectPtr) == sizeof(uint64));
Out.Add(GUE.Members.Id, reinterpret_cast<const uint64&>(In));
}
static void Load(FWeakObjectPtr& Out, FStructLoadView In)
{
LoadSole<uint64>(&Out, In);
}
static FSoleMemberSpec Spec(FSoftObjectPtr*, FDeclId Id)
{
return {Id, GUE.Members.Id, FMemberSpec(GUE.Structs.SoftObjectPath)};
}
static void Save(FMemberBuilder& Out, const FSoftObjectPtr& In, const FSaveContext& Ctx)
{
FBuiltStruct* SoftPath = SaveStruct(&In.GetUniqueID(), GUE.Structs.SoftObjectPath, Ctx);
Out.AddStruct(GUE.Members.Id, GUE.Structs.SoftObjectPath, SoftPath);
}
static void Load(FSoftObjectPtr& Out, FStructLoadView In)
{
Out.ResetWeakPtr();
LoadSoleStruct(&Out.GetUniqueID(), In);
}
static FSoleMemberSpec Spec(FLazyObjectPtr*, FDeclId Id)
{
return {Id, GUE.Members.Id, FMemberSpec(GUE.Structs.Guid)};
}
static void Save(FMemberBuilder& Out, const FLazyObjectPtr& In, const FSaveContext& Ctx)
{
FBuiltStruct* Guid = SaveStruct(&In.GetUniqueID(), GUE.Structs.Guid, Ctx);
Out.AddStruct(GUE.Members.Id, GUE.Structs.Guid, Guid);
}
static void Load(FLazyObjectPtr& Out, FStructLoadView In)
{
Out.ResetWeakPtr();
LoadSoleStruct(&Out.GetUniqueID(), In);
}
};
inline bool DiffProperty(FName A, FName B) { return !A.IsEqual(B, ENameCase::CaseSensitive); }
inline bool DiffProperty(const FText& A, const FText& B) { return !FTextProperty::Identical_Implementation(A, B, 0); }
inline bool DiffProperty(FObjectHandle A, FObjectHandle B) { return A != B; }
inline bool DiffProperty(const FWeakObjectPtr& A, const FWeakObjectPtr& B) { return A != B; }
inline bool DiffProperty(const FSoftObjectPtr& A, const FSoftObjectPtr& B) { return A != B; }
inline bool DiffProperty(const FLazyObjectPtr& A, const FLazyObjectPtr& B) { return A != B; }
template<class Type, class BatchType>
struct TCustomPropertyBinding final : ICustomBinding
{
TCustomPropertyBinding(BatchType& InBatch) : Batch(InBatch) {}
BatchType& Batch;
virtual void SaveCustom(FMemberBuilder& Dst, const void* Src, const void* Default, const FSaveContext& Ctx) override
{
if (!Default || DiffCustom(Src, Default, Ctx))
{
Batch.Save(Dst, *static_cast<const Type*>(Src), Ctx);
}
}
virtual void LoadCustom(void* Dst, FStructLoadView Src, ECustomLoadMethod) const override
{
Batch.Load(*static_cast<Type*>(Dst), Src);
}
virtual bool DiffCustom(const void* A, const void* B, const FBindContext&) const override
{
return DiffProperty(*static_cast<const Type*>(A), *static_cast<const Type*>(B));
}
};
template<class BatchType>
struct TCustomPropertyBindings
{
TCustomPropertyBindings(BatchType& Batch, const FCustomBindings& Underlay)
: Overlay(Underlay)
, Name(Batch)
, Text(Batch)
, ObjectPtr(Batch)
, SoftObjectPtr(Batch)
, WeakObjectPtr(Batch)
, LazyObjectPtr(Batch)
{
Bind(GUE.Structs.Name, Name);
Bind(GUE.Structs.Text, Text);
Bind(GUE.Structs.ClassPtr, ObjectPtr); // TSubclassOf<> is essentially a TObjectPtr
Bind(GUE.Structs.ObjectPtr, ObjectPtr);
Bind(GUE.Structs.SoftObjectPtr, SoftObjectPtr);
Bind(GUE.Structs.WeakObjectPtr, WeakObjectPtr);
Bind(GUE.Structs.LazyObjectPtr, LazyObjectPtr);
}
template<class Type>
void Bind(FDualStructId Id, TCustomPropertyBinding<Type, BatchType>& Binding)
{
Overlay.BindStruct(Id, Binding, Declare(BatchType::Spec((Type*)nullptr, Id)), {});
}
FCustomBindingsOverlay Overlay;
TCustomPropertyBinding<FName, BatchType> Name;
TCustomPropertyBinding<FText, BatchType> Text;
TCustomPropertyBinding<FObjectHandle, BatchType> ObjectPtr;
TCustomPropertyBinding<FSoftObjectPtr, BatchType> SoftObjectPtr;
TCustomPropertyBinding<FWeakObjectPtr, BatchType> WeakObjectPtr;
TCustomPropertyBinding<FLazyObjectPtr, BatchType> LazyObjectPtr;
};
////////////////////////////////////////////////////////////////////////////////////////////////
struct FMemoryBatch
{
TArray64<uint8> Data;
TArray<FStructId> RuntimeIds; // To avoid reindexing schema FType
FMemoryPropertyBatch Properties; // Contains FTexts referenced from Data
};
static constexpr uint32 Magics[] = { 0xFEEDF00D, 0xABCD1234, 0xDADADAAA, 0x99887766, 0xF0F1F2F3 };
volatile static const UObject* GDebugNoteObject;
class FBatchSaver
{
public:
FBatchSaver(FCustomBindings& Customs, int32 NumReserve)
: FlatCtx{{GUE.Schemas, Customs}, Scratch, nullptr}
, DeltaCtx{{GUE.Schemas, Customs}, Scratch, &GUE.Defaults}
{
SavedObjects.Reserve(NumReserve);
}
FORCENOINLINE void Save(FBindId Id, const UObject* Object, const UObject* Arch)
{
FBuiltStruct* Built = Arch ? SaveStructDelta(Object, Arch, Id, DeltaCtx) : SaveStruct(Object, Id, FlatCtx);
SavedObjects.Emplace(Id, Built, Object);
}
TArray64<uint8> Write(TArray<FStructId>* OutRuntimeIds = nullptr) const
{
ESchemaFormat Format = OutRuntimeIds ? ESchemaFormat::InMemoryNames : ESchemaFormat::StableNames;
// Build partial schemas
const FBindDeclarations Types(GUE.Enums, FlatCtx.Customs, GUE.Schemas);
FSchemasBuilder SchemaBuilders(GUE.Names, Types, Scratch, Format);
for (const FSavedObject& Object : SavedObjects)
{
GDebugNoteObject = Object.Input;
SchemaBuilders.NoteStructAndMembers(Object.Id, *Object.Built);
}
GDebugNoteObject = nullptr;
FBuiltSchemas Schemas = SchemaBuilders.Build();
// Save schema ids on the side when using InMemoryNames
if (OutRuntimeIds)
{
*OutRuntimeIds = ExtractRuntimeIds(Schemas);
}
FWriter Writer(GUE.Names, Types, Schemas, Format);
TArray64<uint8> Out;
// Write out FNames when using StableNames
if (!OutRuntimeIds)
{
TArray<FSensitiveName> UsedNames;
UsedNames.Reserve(Writer.GetUsedNames().Num());
for (FNameId Name : Writer.GetUsedNames())
{
UsedNames.Add(GUE.Names.ResolveName(Name));
}
WriteInt(Out, Magics[0]);
WriteNumAndArray(Out, TArrayView<const FSensitiveName, int32>(UsedNames));
}
// Write schemas
WriteInt(Out, Magics[1]);
WriteAlignmentPadding<uint32>(Out);
TArray64<uint8> Tmp;
Writer.WriteSchemas(/* Out */ Tmp);
WriteNumAndArray(Out, TArrayView<const uint8, int64>(Tmp));
Tmp.Reset();
// Write objects
WriteInt(Out, Magics[2]);
for (const FSavedObject& Object : SavedObjects)
{
WriteInt(/* out */ Tmp, Magics[3]);
WriteInt(/* out */ Tmp, Writer.GetWriteId(Object.Id).Get().Idx);
Writer.WriteMembers(/* out */ Tmp, Object.Id, *Object.Built);
WriteSkippableSlice(Out, Tmp);
Tmp.Reset();
}
// Write object terminator
WriteSkippableSlice(Out, TConstArrayView64<uint8>());
WriteInt(Out, Magics[4]);
return Out;
}
private:
struct FSavedObject
{
FBindId Id;
FBuiltStruct* Built;
const UObject* Input; // For debug
};
TArray<FSavedObject> SavedObjects;
mutable FScratchAllocator Scratch;
FSaveContext FlatCtx;
FSaveContext DeltaCtx;
template<typename ArrayType>
static void WriteNumAndArray(TArray64<uint8>& Out, const ArrayType& Items)
{
WriteInt(Out, IntCastChecked<uint32>(Items.Num()));
WriteArray(Out, Items);
}
};
class FMemoryBatchLoader
{
public:
FMemoryBatchLoader(const FCustomBindings& Customs, FMemoryView Data, TConstArrayView<FStructId> RuntimeIds)
{
// Read and mount schemas
FByteReader It(Data);
check(It.Grab<uint32>() == Magics[1]);
It.SkipAlignmentPadding<uint32>();
uint32 SchemasSize = It.Grab<uint32>();
const FSchemaBatch* SavedSchemas = ValidateSchemas(It.GrabSlice(SchemasSize));
check(It.Grab<uint32>() == Magics[2]);
FSchemaBatchId Batch = MountReadSchemas(SavedSchemas);
// Read objects
while (uint64 NumBytes = It.GrabVarIntU())
{
FByteReader ObjIt(It.GrabSlice(NumBytes));
check(ObjIt.Grab<uint32>() == Magics[3]);
FStructSchemaId Id = { ObjIt.Grab<uint32>() };
Objects.Add({ { Id, Batch }, ObjIt });
}
check(It.Grab<uint32>() == Magics[4]);
check(!Objects.IsEmpty());
// Finally create load plans
Plans = CreateLoadPlans(Batch, Customs, GUE.Schemas, RuntimeIds, ESchemaFormat::InMemoryNames);
}
~FMemoryBatchLoader()
{
check(LoadIdx == Objects.Num()); // Test should load all saved objects
Plans.Reset();
UnmountReadSchemas(Objects[0].Schema.Batch);
}
FORCENOINLINE void Load(UObject* Dst)
{
FStructView In = Objects[LoadIdx];
LoadStruct(Dst, In.Values, In.Schema.Id, *Plans);
++LoadIdx;
}
FORCENOINLINE void Reload(UObject* Dst, int32 ReloadIdx)
{
FStructView In = Objects[ReloadIdx];
LoadStruct(Dst, In.Values, In.Schema.Id, *Plans);
}
private:
FLoadBatchPtr Plans;
TArray<FStructView> Objects;
int32 LoadIdx = 0;
template<typename T>
static TConstArrayView<T> GrabNumAndArray(/* in-out */ FByteReader& It)
{
uint32 Num = It.Grab<uint32>();
return MakeArrayView(reinterpret_cast<const T*>(It.GrabBytes(Num * sizeof(T))), Num);
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
// Similar to PlainProps FMemoryBatch but for storing FArchive property serialization results
struct FArchivedProperties
{
TArray<uint8> Data;
TArray<FText> Texts;
};
static constexpr uint32 RoundtripPortFlags = PPF_UseDeprecatedProperties | PPF_ForceTaggedSerialization;
// Match FMemoryPropertyBatch somewhat for a fair comparison, e.g. save FText on side and FName as integer
class FPropertyWriter final : public FMemoryWriter
{
TArray<FText>& Texts;
public:
FPropertyWriter(FArchivedProperties& Out)
: FMemoryWriter(Out.Data)
, Texts(Out.Texts)
{
SetPortFlags(RoundtripPortFlags);
}
FORCENOINLINE void WriteProperties(UObject* Object, UObject* Defaults)
{
UClass* Class = Object->GetClass();
Class->SerializeTaggedProperties(*this, reinterpret_cast<uint8*>(Object), Class, reinterpret_cast<uint8*>(Defaults));
}
virtual FArchive& operator<<(FText& Value) override
{
int32 Idx = INDEX_NONE;
if (!Value.IsEmpty())
{
Idx = Texts.Num();
Texts.Add(Value);
}
return WriteValue(Idx);
}
virtual FArchive& operator<<(FName& Value) override { return WriteValue(FSensitiveName(Value).ToUnstableInt());}
virtual FArchive& operator<<(UObject*& Value) override { return WriteValue(reinterpret_cast<uint64&>(Value)); }
virtual FArchive& operator<<(FObjectPtr& Value) override { return WriteValue(reinterpret_cast<uint64&>(Value)); }
virtual FArchive& operator<<(FWeakObjectPtr& Value) override { return WriteValue(reinterpret_cast<uint64&>(Value)); }
virtual FArchive& operator<<(FLazyObjectPtr& Value) override { return WriteValue(Value.GetUniqueID()); }
virtual FArchive& operator<<(FSoftObjectPtr& Value) override { Value.GetUniqueID().SerializePath(*this); return *this; }
virtual FArchive& operator<<(FSoftObjectPath& Value) override { Value.SerializePath(*this); return *this; }
virtual FString GetArchiveName() const override { return "FPropertyWriter"; }
template<typename T>
FArchive& WriteValue(T Value)
{
return *this << Value;
}
};
class FPropertyReader final : public FMemoryReader
{
const TArray<FText>& Texts;
public:
FPropertyReader(const FArchivedProperties& Out)
: FMemoryReader(Out.Data)
, Texts(Out.Texts)
{
SetPortFlags(RoundtripPortFlags);
}
FORCENOINLINE void ReadProperties(UObject* Object, UObject* Defaults)
{
UClass* Class = Object->GetClass();
Class->SerializeTaggedProperties(*this, reinterpret_cast<uint8*>(Object), Class, reinterpret_cast<uint8*>(Defaults));
}
virtual FArchive& operator<<(FText& Value) override
{
int32 Idx = ReadValue<int32>();
Value = Idx == INDEX_NONE ? FText::GetEmpty() : Texts[Idx];
return *this;
}
virtual FArchive& operator<<(FName& Value) override
{
Value = FSensitiveName::FromUnstableInt(ReadValue<FSensitiveName::IntType>()).ToName();
return *this;
}
virtual FArchive& operator<<(UObject*& Value) override { reinterpret_cast<uint64&>(Value) = ReadValue<uint64>(); return *this; }
virtual FArchive& operator<<(FObjectPtr& Value) override { reinterpret_cast<uint64&>(Value) = ReadValue<uint64>(); return *this; }
virtual FArchive& operator<<(FWeakObjectPtr& Value) override { reinterpret_cast<uint64&>(Value) = ReadValue<uint64>(); return *this; }
virtual FArchive& operator<<(FLazyObjectPtr& Value) override { Value.ResetWeakPtr(); Value.GetUniqueID() = ReadValue<FUniqueObjectGuid>(); return *this; }
virtual FArchive& operator<<(FSoftObjectPtr& Value) override { Value.ResetWeakPtr(); Value.GetUniqueID().SerializePath(*this); return *this; }
virtual FArchive& operator<<(FSoftObjectPath& Value) override { Value.SerializePath(*this); return *this; }
virtual FString GetArchiveName() const override { return "FPropertyReader"; }
template<typename T>
T ReadValue()
{
T Out;
*this << Out;
return Out;
}
};
////////////////////////////////////////////////////////////////////////////////////////////////
struct FInstance
{
FString PathName;
UObject* Orig;
UObject* Arch;
UObject* Base;
UObject* PP;
UObject* TPS;
UObject* UPS;
FBindId Id;
void Init()
{
UClass* Class = Orig->GetClass();
Class->GetDefaultObject(/* create lazily */ true);
Arch = Orig->GetArchetype();
check(Arch != Orig);
Id = GUE.Names.IndexBindId(IndexType(Class));
}
};
static UObject* MakeEmptyInstance(UObject* Obj, FName Name)
{
FStaticConstructObjectParameters Params(Obj->GetClass());
Params.Outer = Obj->GetOuter();//GetTransientPackage();
Params.Name = Name;
Params.SetFlags = Obj->GetFlags();
Params.Template = Obj->GetArchetype();
Params.bAssumeTemplateIsArchetype = true;
Params.bCopyTransientsFromClassDefaults = true;
return StaticConstructObject_Internal(Params);
}
static bool IncludeClass(UClass* Class)
{
static const FName Exclusions[] = {
"CitySampleUnrealEdEngine", // Cloning MTAccessDetector crash
"GameFeaturePluginStateMachine", // Cloning ensure
"WorldSettings", // QAGame
// CitySample - Enum value 8 is undeclared in /Script/Engine.ERichCurveTangentMode, illegal value detected in /Script/Engine.RichCurveKey::TangentMode
"AnimSequence", "AnimationSequencerDataModel", "MovieSceneControlRigParameterSection",
// Enum value 7 is undeclared in /Script/Text3D.EText3DFontStyleFlags, illegal value detected in /Script/Text3DEditor.Text3DEditorFont::FontStyleFlags
// FontStyleFlags Monospace | Bold | Italic (7 '\a') UnrealEditor-Text3DEditor.dll!EText3DFontStyleFlags
"Text3DEditorFontSubsystem",
};
static const FName SuperExclusions[] = { "LevelScriptActor" };
if (!ShouldBind(Class) || !!Algo::Find(Exclusions, Class->GetFName()))
{
return false;
}
// Exclude IDOs
static constexpr EClassFlags IdoFlags = CLASS_NotPlaceable | CLASS_Hidden | CLASS_HideDropDown;
if (Class->HasAllClassFlags(IdoFlags))
{
return false;
}
for (UStruct* Super = Class->GetInheritanceSuper(); Super; Super = Super->GetInheritanceSuper())
{
if (!!Algo::Find(SuperExclusions, Super->GetFName()))
{
return false;
}
}
return true;
}
FORCENOINLINE static void SavePlainProps(FBatchSaver& Batch, TConstArrayView<FInstance> Instances)
{
for (const FInstance& Instance : Instances)
{
Batch.Save(Instance.Id, Instance.Orig, Instance.Base);
}
}
FORCENOINLINE static void LoadPlainProps(FMemoryBatchLoader& Batch, TConstArrayView<FInstance> Instances)
{
for (const FInstance& Instance : Instances)
{
Batch.Load(Instance.PP);
}
}
template<bool bUps>
FORCENOINLINE void SaveArchive(FPropertyWriter& Archive, TConstArrayView<FInstance> Instances)
{
Archive.SetUseUnversionedPropertySerialization(bUps);
for (const FInstance& Instance : Instances)
{
Archive.WriteProperties(Instance.Orig, Instance.Base);
}
}
template<bool bUps>
FORCENOINLINE void LoadArchive(FPropertyReader& Archive, TConstArrayView<FInstance> Instances)
{
Archive.SetUseUnversionedPropertySerialization(bUps);
for (const FInstance& Instance : Instances)
{
Archive.ReadProperties(bUps ? Instance.UPS : Instance.TPS, Instance.Base);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
class FStableNameBatchIds final : public FStableBatchIds
{
TConstArrayView<FSensitiveName> Names;
public:
FStableNameBatchIds(FSchemaBatchId Batch, TConstArrayView<FSensitiveName> InNames) : FStableBatchIds(Batch), Names(InNames) {}
using FStableBatchIds::AppendString;
virtual uint32 NumNames() const override { return static_cast<uint32>(Names.Num()); }
virtual void AppendString(FUtf8Builder& Out, FNameId Name) const override { Names[Name.Idx].AppendString(Out); }
};
[[nodiscard]] static FSchemaBatchId ParseBatch(
TArray64<uint8>& OutData,
TArray<FStructView>& OutObjects,
FUtf8StringView YamlView)
{
// Parse yaml
ParseYamlBatch(OutData, YamlView);
// Grab and mount parsed schemas
FByteReader It(MakeMemoryView(OutData));
const uint32 SchemasSize = It.Grab<uint32>();
FMemoryView SchemasView = It.GrabSlice(SchemasSize);
const FSchemaBatch* Schemas = ValidateSchemas(SchemasView);
FSchemaBatchId Batch = MountReadSchemas(Schemas);
// Grab parsed objects
while (uint64 NumBytes = It.GrabVarIntU())
{
FByteReader ObjIt(It.GrabSlice(NumBytes));
FStructSchemaId Schema = { ObjIt.Grab<uint32>() };
OutObjects.Add({ { Schema, Batch }, ObjIt });
}
return Batch;
}
static void RoundtripText(
const FBatchIds& BatchIds,
TConstArrayView<FStructView> Objects,
TConstArrayView<FInstance> Instances,
ESchemaFormat Format)
{
check(Objects.Num() == Instances.Num());
// Print yaml
UE_LOGFMT(LogPlainPropsUObject, Display, "Printing to PlainProps text using {Format}...", ToString(Format));
TUtf8StringBuilder<256> Yaml;
Yaml.Reserve(INT_MAX);
PrintYamlBatch(Yaml, BatchIds, Objects);
FUtf8StringView YamlView = Yaml.ToView();
// Write to file
const FString Filename = FPaths::ProjectSavedDir() / TEXT("PlainProps") /
(Format == ESchemaFormat::InMemoryNames ? TEXT("InMemoryNames.yaml") : TEXT("StableNames.yaml"));
if (TUniquePtr<FArchive> FileWriter(IFileManager::Get().CreateFileWriter(*Filename)); FileWriter)
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Writing {KB}KB yaml as {Filename}...", Yaml.Len() >> 10, *Filename);
FileWriter->Serialize((void*)YamlView.GetData(), YamlView.Len());
}
// Parse yaml
UE_LOGFMT(LogPlainPropsUObject, Display, "Parsing PlainProps text using {Format}...", ToString(Format));
TArray64<uint8> Data;
TArray<FStructView> ParsedObjects;
FSchemaBatchId ParsedBatch = ParseBatch(Data, ParsedObjects, YamlView);
if (Format == ESchemaFormat::StableNames)
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Diffing PlainProps parsed objects using {Format}...", ToString(Format));
// Diff schemas
check(!DiffSchemas(BatchIds.GetBatchId(), ParsedBatch));
// Diff objects
check(Objects.Num() == ParsedObjects.Num());
const int32 NumObjects = FMath::Min(Objects.Num(), ParsedObjects.Num());
uint32 NumDiffs = 0;
TUtf8StringBuilder<256> Diffs;
for (int32 I = 0; I < NumObjects; ++I)
{
FStructView In = Objects[I];
FStructView Parsed = ParsedObjects[I];
FReadDiffPath DiffPath;
if (DiffStruct(In, Parsed, DiffPath))
{
PrintDiff(Diffs, BatchIds, DiffPath);
Diffs.Append(" in ");
Diffs.Append(Instances[I].PathName);
Diffs.Append("\n");
++NumDiffs;
}
}
UE_LOGFMT(LogPlainPropsUObject, Display,
"Detected {Diffs} diffs in {Objs} PlainProps parsed objects from {KB}KB yaml text using StableNames\n{Diffs}",
NumDiffs, NumObjects, Yaml.Len() >> 10, Diffs.ToString());
}
// Unmount parsed schemas
UnmountReadSchemas(ParsedBatch);
}
class FBatchTextRoundtripper
{
public:
FBatchTextRoundtripper(FMemoryView Data, ESchemaFormat InFormat) : Format(InFormat)
{
FByteReader It(Data);
// Read FNames when using Stable Names
TConstArrayView<FSensitiveName> Names;
if (Format == ESchemaFormat::StableNames)
{
verify(It.Grab<uint32>() == Magics[0]);
Names = GrabNumAndArray<FSensitiveName>(It);
}
// Read and mount schemas
check(It.Grab<uint32>() == Magics[1]);
It.SkipAlignmentPadding<uint32>();
const uint32 SchemasSize = It.Grab<uint32>();
FMemoryView SavedSchemasView = It.GrabSlice(SchemasSize);
const FSchemaBatch* SavedSchemas = ValidateSchemas(SavedSchemasView);
check(It.Grab<uint32>() == Magics[2]);
FSchemaBatchId Batch = MountReadSchemas(SavedSchemas);
// Read objects
while (uint64 NumBytes = It.GrabVarIntU())
{
FByteReader ObjIt(It.GrabSlice(NumBytes));
check(ObjIt.Grab<uint32>() == Magics[3]);
FStructSchemaId Id = { ObjIt.Grab<uint32>() };
Objects.Add({ { Id, Batch }, ObjIt });
}
check(It.Grab<uint32>() == Magics[4]);
check(!Objects.IsEmpty());
// Create BatchIds
if (Format == ESchemaFormat::StableNames)
{
BatchIds = MakeUnique<FStableNameBatchIds>(Batch, Names);
}
else
{
BatchIds = MakeUnique<FMemoryBatchIds>(Batch, GUE.Names);
}
}
~FBatchTextRoundtripper()
{
UnmountReadSchemas(BatchIds->GetBatchId());
}
void RoundtripText(TConstArrayView<FInstance> Instances) const
{
PlainProps::UE::RoundtripText(*BatchIds, Objects, Instances, Format);
}
private:
TArray<FStructView> Objects;
TUniquePtr<FBatchIds> BatchIds;
ESchemaFormat Format;
template<typename T>
static TConstArrayView<T> GrabNumAndArray(/* in-out */ FByteReader& It)
{
uint32 Num = It.Grab<uint32>();
return MakeArrayView(reinterpret_cast<const T*>(It.GrabBytes(Num * sizeof(T))), Num);
}
};
////////////////////////////////////////////////////////////////////////////////
struct FDiffDebug
{
volatile FInstance* Last;
volatile const UTF8CHAR* Str;
};
static FDiffDebug GPpDiff, GTpsDiff, GUpsDiff;
static int32 Roundtrip(ERoundtrip Options)
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Gathering all non-empty UObjects...");
static constexpr EObjectFlags SkipFlags = RF_ClassDefaultObject | RF_MirroredGarbage | RF_InheritableComponentTemplate;
TArray<FInstance> Instances;
for (TObjectIterator<UObject> It(SkipFlags); It; ++It)
{
UObject* Object = *It;
if (IncludeClass(Object->GetClass()))
{
Instances.Emplace(Object->GetPathName(), Object);
}
}
UE_LOGFMT(LogPlainPropsUObject, Display, "Sorting {Num} UObjects ...", Instances.Num());
Algo::Sort(Instances, [](const FInstance& A, const FInstance& B)
{
return FPlatformString::Strcmp(*A.PathName, *B.PathName) < 0;
});
// Create CDOs if needed and then clones for PP and TPS tests
UE_LOGFMT(LogPlainPropsUObject, Display, "Cloning {Num} UObjects up to 4 times...", Instances.Num());
for (FInstance& Instance : Instances)
{
Instance.Init();
}
FlushAsyncLoading();
for (FInstance& Instance : Instances)
{
uint32 N = 1 + &Instance - Instances.GetData();
Instance.Base = Instance.Arch ? MakeEmptyInstance(Instance.Orig, FName("Base", N)) : nullptr;
if (EnumHasAnyFlags(Options, ERoundtrip::PP))
{
Instance.PP = MakeEmptyInstance(Instance.Orig, FName("PP", N));
}
if (EnumHasAnyFlags(Options, ERoundtrip::TPS))
{
Instance.TPS = MakeEmptyInstance(Instance.Orig, FName("TPS", N));
}
if (EnumHasAnyFlags(Options, ERoundtrip::UPS))
{
Instance.UPS = MakeEmptyInstance(Instance.Orig, FName("UPS", N));
}
}
// Save
UE_LOGFMT(LogPlainPropsUObject, Display, "Saving UObjects to PlainProps with InMemoryNames...");
FMemoryBatch Plain;
TCustomPropertyBindings<FMemoryPropertyBatch> Customs(Plain.Properties, GUE.Customs);
{
FBatchSaver Batch(Customs.Overlay, GUObjectArray.GetObjectArrayNum());
SavePlainProps(Batch, Instances);
Plain.Data = Batch.Write(/* out */ &Plain.RuntimeIds);
if (EnumHasAnyFlags(Options, ERoundtrip::TextMemory))
{
FBatchTextRoundtripper MemoryBatch(MakeMemoryView(Plain.Data), ESchemaFormat::InMemoryNames);
MemoryBatch.RoundtripText(Instances);
}
if (EnumHasAnyFlags(Options, ERoundtrip::TextStable))
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Saving UObjects to PlainProps with StableNames...");
TArray64<uint8> StableData = Batch.Write();
FBatchTextRoundtripper StableBatch(MakeMemoryView(StableData), ESchemaFormat::StableNames);
StableBatch.RoundtripText(Instances);
}
}
// Load
uint32 NumPpDiffs = 0;
if (EnumHasAnyFlags(Options, ERoundtrip::PP))
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading UObjects from PlainProps...");
FMemoryBatchLoader Batch(Customs.Overlay, MakeMemoryView(Plain.Data), Plain.RuntimeIds);
LoadPlainProps(Batch, Instances);
// Diff original vs PlainProps
UE_LOGFMT(LogPlainPropsUObject, Display, "Diffing UObjects roundtripped via PlainProps...");
FDiffContext DiffCtx = {{ GUE.Schemas, Customs.Overlay }};
TUtf8StringBuilder<256> PpDiffs;
for (FInstance& Instance : Instances)
{
if (DiffStructs(Instance.Orig, Instance.PP, Instance.Id, /* in-out */ DiffCtx))
{
GPpDiff.Last = &Instance;
PrintDiff(/* out */ PpDiffs, GUE.Names, DiffCtx.Out);
//Batch.Reload(Instance.UPS, &Instance - Instances.GetData());
DiffCtx.Out.Reset();
PpDiffs.Append(" in ");
PpDiffs.Append(Instance.Orig->GetFullName());
PpDiffs.Append("\n");
++NumPpDiffs;
}
}
GPpDiff.Str = PpDiffs.GetData();
UE_LOGFMT(LogPlainPropsUObject, Display, "Detected {Diffs} diffs in {Objs} UObjects saved in a {KB}KB value stream using PlainProps\n{DiffText}", NumPpDiffs, Instances.Num(), Plain.Data.NumBytes() / 1024, PpDiffs.ToString());
}
FArchivedProperties Tps;
if (EnumHasAnyFlags(Options, ERoundtrip::TPS))
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Saving UObjects to TPS archive...");
FPropertyWriter Archive(/* out */ Tps);
SaveArchive<false>(Archive, Instances);
}
if (EnumHasAnyFlags(Options, ERoundtrip::TPS))
{
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading UObjects from TPS archive...");
FPropertyReader Archive(Tps);
LoadArchive<false>(Archive, Instances);
}
static const FName SkipClasses[] = {
"BodySetup", // Skips some structs due to native FCollisionResponse::operator==
"NiagaraScript", "NiagaraNodeFunctionCall", "NiagaraMeshRendererProperties", // FNiagaraTypeDefinition::Serialize resets ClassStructOrEnum
};
if (EnumHasAnyFlags(Options, ERoundtrip::TPS))
{
// Diff original vs TPS
UE_LOGFMT(LogPlainPropsUObject, Display, "Diffing UObjects roundtripped via TPS...");
FDiffContext DiffCtx = {{ GUE.Schemas, Customs.Overlay }};
TUtf8StringBuilder<256> TpsDiffs;
TArray<int32> TpsDiffIdxs;
for (FInstance& Instance : Instances)
{
if (Algo::Find(SkipClasses, Instance.Orig->GetClass()->GetFName()))
{
continue;
}
if (DiffStructs(Instance.Orig, Instance.TPS, Instance.Id, /* in-out */ DiffCtx))
{
GTpsDiff.Last = &Instance;
PrintDiff(/* out */ TpsDiffs, GUE.Names, DiffCtx.Out);
DiffCtx.Out.Reset();
TpsDiffs.Append(" in ");
TpsDiffs.Append(Instance.Orig->GetFullName());
TpsDiffs.Append("\n");
TpsDiffIdxs.Add(&Instance - &Instances[0]);
//FArchivedProperties Tmp;
//FPropertyWriter(/* out */ Tmp).WriteProperties(Instance.Orig, Instance.Base);
//FPropertyReader(Tmp).ReadProperties(Instance.UPS, Instance.Base);
//FDiffContext TmpCtx = DiffCtx;
//bool bOU = DiffStructs(Instance.Id, Instance.Orig, Instance.UPS, /* in-out */ TmpCtx);
//bool bOT = DiffStructs(Instance.Id, Instance.Orig, Instance.TPS, /* in-out */ TmpCtx);
//bool bOP = DiffStructs(Instance.Id, Instance.Orig, Instance.PP, /* in-out */ TmpCtx);
//TUtf8StringBuilder<256> TmpDiff;
//PrintDiff(/* out */ TmpDiff, TmpCtx.Out, GUE.Names);
}
}
GTpsDiff.Str = TpsDiffs.GetData();
UE_LOGFMT(LogPlainPropsUObject, Display, "Detected {Diffs} diffs in {Objs} UObjects saved in a {KB}KB value stream using TPS", TpsDiffIdxs.Num(), Instances.Num(), Tps.Data.NumBytes() / 1024);
}
if (EnumHasAnyFlags(Options, ERoundtrip::UPS))
{
FArchivedProperties Ups;
UE_LOGFMT(LogPlainPropsUObject, Display, "Saving UObjects to UPS archive...");
{
FPropertyWriter Archive(/* out */ Ups);
SaveArchive<true>(Archive, Instances);
}
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading UObjects from UPS archive...");
{
FPropertyReader Archive(Ups);
LoadArchive<true>(Archive, Instances);
}
// Diff original vs UPS
UE_LOGFMT(LogPlainPropsUObject, Display, "Diffing UObjects roundtripped via UPS...");
FDiffContext DiffCtx = {{ GUE.Schemas, Customs.Overlay }};
TUtf8StringBuilder<256> UpsDiffs;
TArray<int32> UpsDiffIdxs;
for (FInstance& Instance : Instances)
{
if (Algo::Find(SkipClasses, Instance.Orig->GetClass()->GetFName()))
{
continue;
}
if (DiffStructs(Instance.Orig, Instance.UPS, Instance.Id, /* in-out */ DiffCtx))
{
GUpsDiff.Last = &Instance;
PrintDiff(/* out */ UpsDiffs, GUE.Names, DiffCtx.Out);
DiffCtx.Out.Reset();
UpsDiffs.Append(" in ");
UpsDiffs.Append(Instance.Orig->GetFullName());
UpsDiffs.Append("\n");
UpsDiffIdxs.Add(&Instance - &Instances[0]);
}
}
GUpsDiff.Str = UpsDiffs.GetData();
UE_LOGFMT(LogPlainPropsUObject, Display, "Detected {Diffs} diffs in {Objs} UObjects saved in a {KB}KB value stream using UPS", UpsDiffIdxs.Num(), Instances.Num(), Ups.Data.NumBytes() / 1024);
}
return NumPpDiffs;
}
static int32 TestBindings(ERoundtrip Options)
{
TScopedStructBinding<FTransform, FDefaultRuntime> Transform;
TScopedStructBinding<FGuid, FDefaultRuntime> Guid;
TScopedStructBinding<FColor, FDefaultRuntime> Color;
TScopedStructBinding<FLinearColor, FDefaultRuntime> LinearColor;
TScopedStructBinding<FFieldPath, FDefaultRuntime> FieldPath(GUE.Structs.FieldPath);
TScopedStructBinding<FScriptDelegate, FDefaultRuntime> Delegate(GUE.Structs.Delegate);
// MulticastDelegate declaration is shared with MulticastSparseDelegate
TScopedStructBinding<FMulticastScriptDelegate, FDefaultRuntime> InlineMulticast({GUE.Structs.MulticastInlineDelegate, GUE.Structs.MulticastDelegate});
// Verse
TScopedStructBinding<FVerseFunction, FDefaultRuntime> VerseFunction(GUE.Structs.VerseFunction);
TScopedStructBinding<::UE::FDynamicallyTypedValue, FDefaultRuntime> DynamicallyTypedValue(GUE.Structs.DynamicallyTypedValue);
TScopedStructBinding<FReferencePropertyValue, FDefaultRuntime> ReferencePropertyValue(GUE.Structs.ReferencePropertyValue);
GUE.Defaults.BindZeroes(GUE.Structs.FieldPath, sizeof(FFieldPath), alignof(FFieldPath));
InitBatchedProperties();
BindInitialTypes();
return Roundtrip(Options);
}
} // namespace PlainProps::UE
#include "PlainPropsCommandlets.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Engine/World.h"
UTestPlainPropsCommandlet::UTestPlainPropsCommandlet(const FObjectInitializer& Init)
: Super(Init)
{}
int32 UTestPlainPropsCommandlet::Main(const FString& Params)
{
using namespace PlainProps;
using namespace PlainProps::UE;
DbgVis::FIdScope _(GUE.Names, "SensName");
if (int32 LoadIdx = Params.Find("-load="); LoadIdx != INDEX_NONE)
{
// E.g. -run=TestPlainProps -load=/BRRoot/BRRoot.BRRoot,/Game/Maps/FrontEnd.FrontEnd
const TCHAR* It = &Params[LoadIdx + 6];
FStringView Assets(It, FAsciiSet::FindFirstOrEnd(It, FAsciiSet(" ")) - It);
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading {Assets}...", Assets);
int32 CommaIndex;
while (Assets.FindChar(',', CommaIndex))
{
FSoftObjectPath(Assets.Left(CommaIndex)).LoadAsync({});
Assets.RightChopInline(CommaIndex + 1);
}
FSoftObjectPath(Assets).LoadAsync({});
}
else if (Params.Find("-loadmaps") != INDEX_NONE)
{
// load all .umaps in asset registry
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading asset registry...");
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName);
AssetRegistryModule.Get().SearchAllAssets(true);
UE_LOGFMT(LogPlainPropsUObject, Display, "Gathering all maps...");
TArray<FAssetData> Maps;
AssetRegistryModule.Get().GetAssetsByClass(UWorld::StaticClass()->GetClassPathName(), /* out */ Maps, true);
UE_LOGFMT(LogPlainPropsUObject, Display, "Loading all {Maps} maps...", Maps.Num());
for (FAssetData& Map : Maps)
{
Map.GetSoftObjectPath().LoadAsync({});
}
}
FlushAsyncLoading();
UE_LOGFMT(LogPlainPropsUObject, Display, "Starting test...");
ERoundtrip Options = ERoundtrip::PP | ERoundtrip::UPS | ERoundtrip::TPS | ERoundtrip::TextMemory;
if (Params.Find("-pp") != INDEX_NONE)
{
Options = ERoundtrip::PP | ERoundtrip::TextMemory;
}
else if (Params.Find("-text") != INDEX_NONE)
{
Options = ERoundtrip::TextMemory | ERoundtrip::TextStable;
}
else if (Params.Find("-notext") != INDEX_NONE)
{
EnumRemoveFlags(Options, ERoundtrip::TextMemory | ERoundtrip::TextStable);
}
return TestBindings(Options);
}