Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Kismet2/ReloadUtilities.cpp
2025-05-18 13:04:45 +08:00

1347 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
ReloadUtilities.cpp: Helpers for reloading
=============================================================================*/
#include "Kismet2/ReloadUtilities.h"
#include "AssetCompilingManager.h"
#include "Async/AsyncWork.h"
#include "Engine/Engine.h"
#include "Engine/EngineTypes.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Kismet2/KismetReinstanceUtilities.h"
#include "Misc/QueuedThreadPool.h"
#include "Serialization/ArchiveReplaceObjectRef.h"
#include "UObject/Class.h"
#include "UObject/UObjectIterator.h"
#include "Misc/StringBuilder.h"
namespace UE::Reload::Private
{
/** Holds a property and its offset in the serialized properties data array */
struct FCDOProperty
{
FCDOProperty()
: Property(nullptr)
, SubobjectName(NAME_None)
, SerializedValueOffset(0)
, SerializedValueSize(0)
{}
FProperty* Property;
FName SubobjectName;
int64 SerializedValueOffset;
int64 SerializedValueSize;
};
/** Contains all serialized CDO property data and the map of all serialized properties */
struct FCDOPropertyData
{
TArray<uint8> Bytes;
TMap<FName, FCDOProperty> Properties;
};
/**
* Helper class used for re-instancing native and blueprint classes after hot-reload
*/
class FReloadClassReinstancer : public FBlueprintCompileReinstancer
{
/** Hot-reloaded version of the old class */
UClass* NewClass;
/** Necessary for delta serialization */
TObjectPtr<UObject> CopyOfPreviousCDO;
/**
* Sets the re-instancer up for new class re-instancing
*
* @param InNewClass Class that has changed after hot-reload
* @param InOldClass Class before it was hot-reloaded
*/
void SetupNewClassReinstancing(UClass* InNewClass, UClass* InOldClass);
/**
* Sets the re-instancer up for old class re-instancing. Always re-creates the CDO.
*
* @param InOldClass Class that has NOT changed after hot-reload
*/
void RecreateCDOAndSetupOldClassReinstancing(UClass* InOldClass);
public:
/** Sets the re-instancer up to re-instance native classes */
FReloadClassReinstancer(UClass* InNewClass, UClass* InOldClass, UObject* InOldClassCDO,
const TSet<UObject*>& InReinstancingObjects, TSet<UBlueprint*>& InCompiledBlueprints);
/** Destructor */
virtual ~FReloadClassReinstancer();
// FSerializableObject interface
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
// End of FSerializableObject interface
virtual bool IsClassObjectReplaced() const override { return true; }
virtual void BlueprintWasRecompiled(UBlueprint* BP, bool bBytecodeOnly) override;
protected:
// FBlueprintCompileReinstancer interface
virtual bool ShouldPreserveRootComponentOfReinstancedActor() const override { return false; }
// End of FBlueprintCompileReinstancer interface
private:
/** Collection of blueprints already recompiled */
TSet<UBlueprint*>& CompiledBlueprints;
};
FReloadClassReinstancer::FReloadClassReinstancer(UClass* InNewClass, UClass* InOldClass, UObject* InOldClassCDO, const TSet<UObject*>& InReinstancingObjects, TSet<UBlueprint*>& InCompiledBlueprints)
: NewClass(nullptr)
, CopyOfPreviousCDO(nullptr)
, CompiledBlueprints(InCompiledBlueprints)
{
ensure(InOldClass);
ensure(!HotReloadedOldClass && !HotReloadedNewClass);
HotReloadedOldClass = InOldClass;
HotReloadedNewClass = InNewClass ? InNewClass : InOldClass;
OriginalCDO = InOldClassCDO;
for (UObject* Object : InReinstancingObjects)
{
ObjectsThatShouldUseOldStuff.Add(Object);
}
// If InNewClass is NULL, then the old class has not changed after hot-reload.
// However, we still need to check for changes to its constructor code (CDO values).
if (InNewClass)
{
SetupNewClassReinstancing(InNewClass, InOldClass);
TMap<UObject*, UObject*> ClassRedirects;
ClassRedirects.Add(InOldClass, InNewClass);
for (TObjectIterator<UBlueprint> BlueprintIt; BlueprintIt; ++BlueprintIt)
{
constexpr EArchiveReplaceObjectFlags ReplaceObjectArchFlags = (EArchiveReplaceObjectFlags::IgnoreOuterRef | EArchiveReplaceObjectFlags::IgnoreArchetypeRef);
FArchiveReplaceObjectRef<UObject> ReplaceObjectArch(*BlueprintIt, ClassRedirects, ReplaceObjectArchFlags);
}
}
else
{
RecreateCDOAndSetupOldClassReinstancing(InOldClass);
}
}
FReloadClassReinstancer::~FReloadClassReinstancer()
{
// Make sure the base class does not remove the DuplicatedClass from root, we not always want it.
// For example when we're just reconstructing CDOs. Other cases are handled by HotReloadClassReinstancer.
DuplicatedClass = nullptr;
ensure(HotReloadedOldClass);
HotReloadedOldClass = nullptr;
HotReloadedNewClass = nullptr;
}
void FReloadClassReinstancer::SetupNewClassReinstancing(UClass* InNewClass, UClass* InOldClass)
{
// Set base class members to valid values
ClassToReinstance = InNewClass;
DuplicatedClass = InOldClass;
bHasReinstanced = false;
NewClass = InNewClass;
SaveClassFieldMapping(InOldClass);
ObjectsThatShouldUseOldStuff.Add(InOldClass); //CDO of REINST_ class can be used as archetype
TArray<UClass*> ChildrenOfClass;
GetDerivedClasses(InOldClass, ChildrenOfClass);
for (auto ClassIt = ChildrenOfClass.CreateConstIterator(); ClassIt; ++ClassIt)
{
UClass* ChildClass = *ClassIt;
UBlueprint* ChildBP = Cast<UBlueprint>(ChildClass->ClassGeneratedBy);
if (ChildBP && !ChildBP->HasAnyFlags(RF_BeingRegenerated))
{
// If this is a direct child, change the parent and relink so the property chain is valid for reinstancing
if (!ChildBP->HasAnyFlags(RF_NeedLoad))
{
if (ChildClass->GetSuperClass() == InOldClass)
{
ReparentChild(ChildBP);
}
Children.AddUnique(ChildBP);
if (ChildBP->ParentClass == InOldClass)
{
ChildBP->ParentClass = NewClass;
}
}
else
{
// If this is a child that caused the load of their parent, relink to the REINST class so that we can still serialize in the CDO, but do not add to later processing
ReparentChild(ChildClass);
}
}
}
// Finally, remove the old class from Root so that it can get GC'd and mark it as CLASS_NewerVersionExists
InOldClass->RemoveFromRoot();
InOldClass->ClassFlags |= CLASS_NewerVersionExists;
}
void FReloadClassReinstancer::RecreateCDOAndSetupOldClassReinstancing(UClass* InOldClass)
{
// Set base class members to valid values
ClassToReinstance = InOldClass;
DuplicatedClass = InOldClass;
bHasReinstanced = false;
NewClass = InOldClass; // The class doesn't change in this case
SaveClassFieldMapping(InOldClass);
TArray<UClass*> ChildrenOfClass;
GetDerivedClasses(InOldClass, ChildrenOfClass);
for (auto ClassIt = ChildrenOfClass.CreateConstIterator(); ClassIt; ++ClassIt)
{
UClass* ChildClass = *ClassIt;
UBlueprint* ChildBP = Cast<UBlueprint>(ChildClass->ClassGeneratedBy);
if (ChildBP && !ChildBP->HasAnyFlags(RF_BeingRegenerated))
{
if (!ChildBP->HasAnyFlags(RF_NeedLoad))
{
Children.AddUnique(ChildBP);
UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(ChildBP->GeneratedClass);
UObject* CurrentCDO = BPGC ? BPGC->GetDefaultObject(false) : nullptr;
if (CurrentCDO && (OriginalCDO == CurrentCDO->GetArchetype()))
{
BPGC->OverridenArchetypeForCDO = OriginalCDO;
}
}
}
}
}
void FReloadClassReinstancer::AddReferencedObjects(FReferenceCollector& Collector)
{
FBlueprintCompileReinstancer::AddReferencedObjects(Collector);
Collector.AllowEliminatingReferences(false);
Collector.AddReferencedObject(CopyOfPreviousCDO);
Collector.AllowEliminatingReferences(true);
}
void FReloadClassReinstancer::BlueprintWasRecompiled(UBlueprint* BP, bool bBytecodeOnly)
{
CompiledBlueprints.Add(BP);
FBlueprintCompileReinstancer::BlueprintWasRecompiled(BP, bBytecodeOnly);
}
/**
* Creates a mem-comparable array of data containing CDO property values.
*
* @param InObject CDO
* @param OutData Data containing all of the CDO property values
*/
void SerializeCDOProperties(UObject* InObject, FCDOPropertyData& OutData)
{
// Creates a mem-comparable CDO data
class FCDOWriter : public FMemoryWriter
{
/** Objects already visited by this archive */
TSet<UObject*>& VisitedObjects;
/** Output property data */
FCDOPropertyData& PropertyData;
/** Current subobject being serialized */
FName SubobjectName;
public:
/** Serializes all script properties of the provided DefaultObject */
FCDOWriter(FCDOPropertyData& InOutData, TSet<UObject*>& InVisitedObjects, FName InSubobjectName)
: FMemoryWriter(InOutData.Bytes, /* bIsPersistent = */ false, /* bSetOffset = */ true)
, VisitedObjects(InVisitedObjects)
, PropertyData(InOutData)
, SubobjectName(InSubobjectName)
{
// Disable delta serialization, we want to serialize everything
ArNoDelta = true;
}
virtual void Serialize(void* Data, int64 Num) override
{
// Collect serialized properties so we can later update their values on instances if they change
FProperty* SerializedProperty = GetSerializedProperty();
if (SerializedProperty != nullptr)
{
FCDOProperty& PropertyInfo = PropertyData.Properties.FindOrAdd(SerializedProperty->GetFName());
if (PropertyInfo.Property == nullptr)
{
PropertyInfo.Property = SerializedProperty;
PropertyInfo.SubobjectName = SubobjectName;
PropertyInfo.SerializedValueOffset = Tell();
PropertyInfo.SerializedValueSize = Num;
}
else
{
PropertyInfo.SerializedValueSize += Num;
}
}
FMemoryWriter::Serialize(Data, Num);
}
/** Serializes an object. Only name and class for normal references, deep serialization for DSOs */
virtual FArchive& operator<<(class UObject*& InObj) override
{
FArchive& Ar = *this;
if (InObj)
{
FName ClassName = InObj->GetClass()->GetFName();
FName ObjectName = InObj->GetFName();
Ar << ClassName;
Ar << ObjectName;
if (!VisitedObjects.Contains(InObj))
{
VisitedObjects.Add(InObj);
if (Ar.GetSerializedProperty() && Ar.GetSerializedProperty()->ContainsInstancedObjectProperty())
{
// Serialize all DSO properties too
FCDOWriter DefaultSubobjectWriter(PropertyData, VisitedObjects, InObj->GetFName());
InObj->SerializeScriptProperties(DefaultSubobjectWriter);
Seek(PropertyData.Bytes.Num());
}
}
}
else
{
FName UnusedName = NAME_None;
Ar << UnusedName;
Ar << UnusedName;
}
return *this;
}
virtual FArchive& operator<<(FObjectPtr& InObj) override
{
// Invoke the method above
return FArchiveUObject::SerializeObjectPtr(*this, InObj);
}
/** Serializes an FName as its index and number */
virtual FArchive& operator<<(FName& InName) override
{
FArchive& Ar = *this;
FNameEntryId ComparisonIndex = InName.GetComparisonIndex();
FNameEntryId DisplayIndex = InName.GetDisplayIndex();
int32 Number = InName.GetNumber();
Ar << ComparisonIndex;
Ar << DisplayIndex;
Ar << Number;
return Ar;
}
virtual FArchive& operator<<(FLazyObjectPtr& LazyObjectPtr) override
{
FArchive& Ar = *this;
FUniqueObjectGuid UniqueID = LazyObjectPtr.GetUniqueID();
Ar << UniqueID;
return *this;
}
virtual FArchive& operator<<(FSoftObjectPtr& Value) override
{
FArchive& Ar = *this;
FSoftObjectPath UniqueID = Value.GetUniqueID();
Ar << UniqueID;
return Ar;
}
virtual FArchive& operator<<(FSoftObjectPath& Value) override
{
FArchive& Ar = *this;
FString Path = Value.ToString();
Ar << Path;
if (IsLoading())
{
Value.SetPath(MoveTemp(Path));
}
return Ar;
}
FArchive& operator<<(FWeakObjectPtr& WeakObjectPtr) override
{
return FArchiveUObject::SerializeWeakObjectPtr(*this, WeakObjectPtr);
}
/** Archive name, for debugging */
virtual FString GetArchiveName() const override { return TEXT("FCDOWriter"); }
};
TSet<UObject*> VisitedObjects;
VisitedObjects.Add(InObject);
FCDOWriter Ar(OutData, VisitedObjects, NAME_None);
InObject->SerializeScriptProperties(Ar);
}
/** Returns true if the properties of the CDO have changed during hot-reload */
bool HavePropertiesChanged(const FCDOPropertyData& lhs, const FCDOPropertyData& rhs)
{
return lhs.Bytes.Num() != rhs.Bytes.Num() || FMemory::Memcmp(lhs.Bytes.GetData(), rhs.Bytes.GetData(), lhs.Bytes.Num());
}
/** Helper for finding subobject in an array. Usually there's not that many subobjects on a class to justify a TMap */
UObject* FindDefaultSubobject(TArray<UObject*>& InDefaultSubobjects, FName SubobjectName)
{
for (UObject* Subobject : InDefaultSubobjects)
{
if (Subobject->GetFName() == SubobjectName)
{
return Subobject;
}
}
return nullptr;
}
/** Update the properties from the old class CDO to the new class CDO */
void UpdateDefaultProperties(UClass* NewClass, const FCDOPropertyData& OldClassCDOProperties, const FCDOPropertyData& NewClassCDOProperties)
{
struct FPropertyToUpdate
{
FProperty* Property;
FName SubobjectName;
const uint8* OldSerializedValuePtr;
uint8* NewValuePtr;
int64 OldSerializedSize;
};
/** Memory writer archive that supports UObject values the same way as FCDOWriter. */
class FPropertyValueMemoryWriter : public FMemoryWriter
{
public:
FPropertyValueMemoryWriter(TArray<uint8>& OutData)
: FMemoryWriter(OutData)
{}
virtual FArchive& operator<<(class UObject*& InObj) override
{
FArchive& Ar = *this;
if (InObj)
{
FName ClassName = InObj->GetClass()->GetFName();
FName ObjectName = InObj->GetFName();
Ar << ClassName;
Ar << ObjectName;
}
else
{
FName UnusedName = NAME_None;
Ar << UnusedName;
Ar << UnusedName;
}
return *this;
}
virtual FArchive& operator<<(FObjectPtr& InObj) override
{
// Invoke the method above
return FArchiveUObject::SerializeObjectPtr(*this, InObj);
}
virtual FArchive& operator<<(FName& InName) override
{
FArchive& Ar = *this;
FNameEntryId ComparisonIndex = InName.GetComparisonIndex();
FNameEntryId DisplayIndex = InName.GetDisplayIndex();
int32 Number = InName.GetNumber();
Ar << ComparisonIndex;
Ar << DisplayIndex;
Ar << Number;
return Ar;
}
virtual FArchive& operator<<(FLazyObjectPtr& LazyObjectPtr) override
{
FArchive& Ar = *this;
FUniqueObjectGuid UniqueID = LazyObjectPtr.GetUniqueID();
Ar << UniqueID;
return *this;
}
virtual FArchive& operator<<(FSoftObjectPtr& Value) override
{
FArchive& Ar = *this;
FSoftObjectPath UniqueID = Value.GetUniqueID();
Ar << UniqueID;
return Ar;
}
virtual FArchive& operator<<(FSoftObjectPath& Value) override
{
FArchive& Ar = *this;
FString Path = Value.ToString();
Ar << Path;
if (IsLoading())
{
Value.SetPath(MoveTemp(Path));
}
return Ar;
}
FArchive& operator<<(FWeakObjectPtr& WeakObjectPtr) override
{
return FArchiveUObject::SerializeWeakObjectPtr(*this, WeakObjectPtr);
}
};
// Collect default subobjects to update their properties too
const int32 DefaultSubobjectArrayCapacity = 16;
TArray<UObject*> DefaultSubobjectArray;
DefaultSubobjectArray.Empty(DefaultSubobjectArrayCapacity);
NewClass->GetDefaultObject()->GetDefaultSubobjects(DefaultSubobjectArray);
TArray<FPropertyToUpdate> PropertiesToUpdate;
// Collect all properties that have actually changed
for (const TPair<FName, FCDOProperty>& Pair : NewClassCDOProperties.Properties)
{
const FCDOProperty* OldPropertyInfo = OldClassCDOProperties.Properties.Find(Pair.Key);
if (OldPropertyInfo)
{
const FCDOProperty& NewPropertyInfo = Pair.Value;
const uint8* OldSerializedValuePtr = OldClassCDOProperties.Bytes.GetData() + OldPropertyInfo->SerializedValueOffset;
const uint8* NewSerializedValuePtr = NewClassCDOProperties.Bytes.GetData() + NewPropertyInfo.SerializedValueOffset;
if (OldPropertyInfo->SerializedValueSize != NewPropertyInfo.SerializedValueSize ||
FMemory::Memcmp(OldSerializedValuePtr, NewSerializedValuePtr, OldPropertyInfo->SerializedValueSize) != 0)
{
// Property value has changed so add it to the list of properties that need updating on instances
FPropertyToUpdate PropertyToUpdate;
PropertyToUpdate.Property = NewPropertyInfo.Property;
PropertyToUpdate.NewValuePtr = nullptr;
PropertyToUpdate.SubobjectName = NewPropertyInfo.SubobjectName;
if (NewPropertyInfo.Property->GetOwner<UObject>() == NewClass)
{
PropertyToUpdate.NewValuePtr = PropertyToUpdate.Property->ContainerPtrToValuePtr<uint8>(NewClass->GetDefaultObject());
}
else if (NewPropertyInfo.SubobjectName != NAME_None)
{
UObject* DefaultSubobjectPtr = FindDefaultSubobject(DefaultSubobjectArray, NewPropertyInfo.SubobjectName);
if (DefaultSubobjectPtr && NewPropertyInfo.Property->GetOwner<UObject>() == DefaultSubobjectPtr->GetClass())
{
PropertyToUpdate.NewValuePtr = PropertyToUpdate.Property->ContainerPtrToValuePtr<uint8>(DefaultSubobjectPtr);
}
}
if (PropertyToUpdate.NewValuePtr)
{
PropertyToUpdate.OldSerializedValuePtr = OldSerializedValuePtr;
PropertyToUpdate.OldSerializedSize = OldPropertyInfo->SerializedValueSize;
PropertiesToUpdate.Add(PropertyToUpdate);
}
}
}
}
if (PropertiesToUpdate.Num())
{
TArray<uint8> CurrentValueSerializedData;
// Update properties on all existing instances of the class
const UPackage* TransientPackage = GetTransientPackage();
for (FThreadSafeObjectIterator It(NewClass); It; ++It)
{
UObject* ObjectPtr = *It;
if (!IsValidChecked(ObjectPtr) || ObjectPtr->GetOutermost() == TransientPackage)
{
continue;
}
DefaultSubobjectArray.Empty(DefaultSubobjectArrayCapacity);
ObjectPtr->GetDefaultSubobjects(DefaultSubobjectArray);
for (auto& PropertyToUpdate : PropertiesToUpdate)
{
uint8* InstanceValuePtr = nullptr;
if (PropertyToUpdate.SubobjectName == NAME_None)
{
InstanceValuePtr = PropertyToUpdate.Property->ContainerPtrToValuePtr<uint8>(ObjectPtr);
}
else
{
UObject* DefaultSubobjectPtr = FindDefaultSubobject(DefaultSubobjectArray, PropertyToUpdate.SubobjectName);
if (DefaultSubobjectPtr && PropertyToUpdate.Property->GetOwner<UObject>() == DefaultSubobjectPtr->GetClass())
{
InstanceValuePtr = PropertyToUpdate.Property->ContainerPtrToValuePtr<uint8>(DefaultSubobjectPtr);
}
}
if (InstanceValuePtr)
{
// Serialize current value to a byte array as we don't have the previous CDO to compare against, we only have its serialized property data
CurrentValueSerializedData.Empty(CurrentValueSerializedData.Num() + CurrentValueSerializedData.GetSlack());
FPropertyValueMemoryWriter CurrentValueWriter(CurrentValueSerializedData);
PropertyToUpdate.Property->SerializeItem(FStructuredArchiveFromArchive(CurrentValueWriter).GetSlot(), InstanceValuePtr);
// Update only when the current value on the instance is identical to the original CDO
if (CurrentValueSerializedData.Num() == PropertyToUpdate.OldSerializedSize &&
FMemory::Memcmp(CurrentValueSerializedData.GetData(), PropertyToUpdate.OldSerializedValuePtr, CurrentValueSerializedData.Num()) == 0)
{
// Update with the new value
PropertyToUpdate.Property->CopyCompleteValue(InstanceValuePtr, PropertyToUpdate.NewValuePtr);
}
}
}
}
}
}
/** Helper class that handles class reloading */
class FReloadClassHelper
{
private:
enum class TopologicalState
{
None,
Visited,
Finished,
};
struct ClassReinstanceState
{
UClass* OldClass;
UObject* OldClassCDO;
TopologicalState State = TopologicalState::None;
FCDOPropertyData OldClassCDOProperties;
};
public:
FReloadClassHelper(FOutputDevice& InAr, const TSet<UObject*>& InReinstancingObjects, TSet<UBlueprint*>& InCompiledBlueprints, TMap<UObject*, UObject*>& InReconstructedCDOsMap, TMap<UObject*, UObject*>& InReinstancedCDOsMap);
/**
* Re-instance the collection of classes.
* The map is from old to new class object.
* If the old class is not changing structure, then the new class pointer is null.
*/
void ReinstanceClasses(const TMap<UClass*, UClass*>& ClassesToReinstance);
private:
/** Given a new/old class pair, re-instance the class */
void ReinstanceClass(UClass* NewClass, ClassReinstanceState& State);
/** Scan the properties looking for any referenced classes that need to be re-instanced first */
void ReinstanceClassPropertyScan(FProperty* PropertyLink);
/** Checked the property looking for any referenced classes that need to be re-instanced first */
void ReinstanceClassProperty(FProperty* Property);
private:
FOutputDevice& Ar;
const TSet<UObject*>& ReinstancingObjects;
TSet<UBlueprint*>& CompiledBlueprints;
TMap<UObject*, UObject*>& ReconstructedCDOsMap;
TMap<UObject*, UObject*>& ReinstancedCDOsMap;
TMap<UClass*, ClassReinstanceState> ReinstanceStates;
};
FReloadClassHelper::FReloadClassHelper(FOutputDevice& InAr, const TSet<UObject*>& InReinstancingObjects, TSet<UBlueprint*>& InCompiledBlueprints, TMap<UObject*, UObject*>& InReconstructedCDOsMap, TMap<UObject*, UObject*>& InReinstancedCDOsMap)
: Ar(InAr)
, ReinstancingObjects(InReinstancingObjects)
, CompiledBlueprints(InCompiledBlueprints)
, ReconstructedCDOsMap(InReconstructedCDOsMap)
, ReinstancedCDOsMap(InReinstancedCDOsMap)
{
}
void FReloadClassHelper::ReinstanceClasses(const TMap<UClass*, UClass*>& ClassesToReinstance)
{
// Create the collection of re-instance states so we can re-instance with topological sorting.
for (const TPair<UClass*, UClass*>& Pair : ClassesToReinstance)
{
UClass* NewClass = Pair.Value != nullptr ? Pair.Value : Pair.Key;
UClass* OldClass = Pair.Key;
UObject* OldClassCDO = OldClass->GetDefaultObject();
ClassReinstanceState State{ OldClass, OldClassCDO };
UE::Reload::Private::SerializeCDOProperties(OldClassCDO, State.OldClassCDOProperties);
ReinstanceStates.Add(NewClass, MoveTemp(State));
}
// Rename and clear the old defaults
for (TPair<UClass*, ClassReinstanceState>& Pair : ReinstanceStates)
{
// If we are re-instancing a class that didn't change structure, then we need to replace
// the existing default object
if (Pair.Key == Pair.Value.OldClass)
{
Pair.Value.OldClassCDO->Rename(
*MakeUniqueObjectName(
GetTransientPackage(),
Pair.Value.OldClassCDO->GetClass(),
*FString::Printf(TEXT("BPGC_ARCH_FOR_CDO_%s"), *Pair.Value.OldClass->GetName())
).ToString(),
GetTransientPackage(),
REN_DoNotDirty | REN_DontCreateRedirectors | REN_NonTransactional | REN_SkipGeneratedClasses);
// Clear the class default object so it gets recreated.
Pair.Value.OldClass->SetDefaultObject(nullptr);
}
}
// Re-instance the classes
for (TPair<UClass*, ClassReinstanceState>& Pair : ReinstanceStates)
{
ReinstanceClass(Pair.Key, Pair.Value);
}
}
void FReloadClassHelper::ReinstanceClass(UClass* NewClass, ClassReinstanceState& State)
{
// If this isn't a class we care about or we are already processing, then skip
if (State.State != TopologicalState::None)
{
return;
}
// Mark that we are in the process of re-instancing this class
State.State = TopologicalState::Visited;
// Run the parents
for (UClass* Super = NewClass->GetSuperClass(); Super != nullptr; Super = Super->GetSuperClass())
{
ClassReinstanceState* SuperState = ReinstanceStates.Find(Super);
if (SuperState != nullptr)
{
ReinstanceClass(Super, *SuperState);
}
}
// Look for any properties that reference other types needing re-instancing. They will be re-instanced first
ReinstanceClassPropertyScan(NewClass->PropertyLink);
// Now we re-instance this class
{
UClass* OldClass = State.OldClass;
// Collect the property values of the new CDO
FCDOPropertyData NewClassCDOProperties;
SerializeCDOProperties(NewClass->GetDefaultObject(), NewClassCDOProperties);
// If the structure didn't change, always add to the list of reconstructed CDOs
if (OldClass == NewClass)
{
ReconstructedCDOsMap.Add(State.OldClassCDO, NewClass->GetDefaultObject());
}
// We only need to do re-instancing when we have a new UClass.
else
{
ReinstancedCDOsMap.Add(State.OldClassCDO, NewClass->GetDefaultObject());
UClass* NullableNewClass = NewClass == OldClass ? nullptr : NewClass;
TSharedPtr<FReloadClassReinstancer> ReinstanceHelper = MakeShareable(new FReloadClassReinstancer(
NullableNewClass, OldClass, State.OldClassCDO, ReinstancingObjects, CompiledBlueprints));
Ar.Logf(ELogVerbosity::Log, TEXT("Re-instancing %s after reload."), *NewClass->GetName());
ReinstanceHelper->ReinstanceObjects(true);
}
// Update the default values
UpdateDefaultProperties(NewClass, State.OldClassCDOProperties, NewClassCDOProperties);
}
State.State = TopologicalState::Finished;
}
void FReloadClassHelper::ReinstanceClassPropertyScan(FProperty* PropertyLink)
{
for (FProperty* Property = PropertyLink; Property; Property = Property->PropertyLinkNext)
{
ReinstanceClassProperty(Property);
}
}
void FReloadClassHelper::ReinstanceClassProperty(FProperty* Property)
{
if (Property == nullptr)
{
return;
}
if (FObjectPropertyBase* objProp = CastField<FObjectPropertyBase>(Property))
{
if (objProp->PropertyClass != nullptr)
{
ClassReinstanceState* State = ReinstanceStates.Find(objProp->PropertyClass);
if (State != nullptr)
{
ReinstanceClass(objProp->PropertyClass, *State);
}
}
if (FClassProperty* classProp = CastField<FClassProperty>(Property))
{
if (classProp->MetaClass != nullptr)
{
ClassReinstanceState* State = ReinstanceStates.Find(classProp->MetaClass);
if (State != nullptr)
{
ReinstanceClass(classProp->MetaClass, *State);
}
}
}
}
else if (FStructProperty* structProp = CastField<FStructProperty>(Property))
{
ReinstanceClassPropertyScan(structProp->Struct->PropertyLink);
}
else if (FMapProperty* MapProp = CastField<FMapProperty>(Property))
{
ReinstanceClassProperty(MapProp->KeyProp);
ReinstanceClassProperty(MapProp->ValueProp);
}
else if (FSetProperty* SetProp = CastField<FSetProperty>(Property))
{
ReinstanceClassProperty(SetProp->ElementProp);
}
else if (FArrayProperty* ArrayProp = CastField<FArrayProperty>(Property))
{
ReinstanceClassProperty(ArrayProp->Inner);
}
}
} // namespace UE::Reload::Private
FReload::FReload(EActiveReloadType InType, const TCHAR* InPrefix, const TArray<UPackage*>& InPackages, FOutputDevice& InAr)
: Type(InType)
, Prefix(InPrefix)
, Packages(InPackages)
, Ar(InAr)
, bCollectPackages(false)
{
#if WITH_RELOAD
BeginReload(Type, *this);
#endif
}
FReload::FReload(EActiveReloadType InType, const TCHAR* InPrefix, FOutputDevice& InAr)
: Type(InType)
, Prefix(InPrefix)
, Ar(InAr)
, bCollectPackages(true)
{
#if WITH_RELOAD
BeginReload(Type, *this);
#endif
}
FReload::~FReload()
{
#if WITH_RELOAD
EndReload();
#endif
TStringBuilder<256> Builder;
if (PackageStats.HasValues() || ClassStats.HasValues() || StructStats.HasValues() || EnumStats.HasValues() || NumFunctionsRemapped != 0 || NumScriptStructsRemapped != 0)
{
FormatStats(Builder, TEXT("package"), TEXT("packages"), PackageStats);
FormatStats(Builder, TEXT("class"), TEXT("classes"), ClassStats);
FormatStats(Builder, TEXT("enum"), TEXT("enums"), EnumStats);
FormatStats(Builder, TEXT("scriptstruct"), TEXT("scriptstructs"), StructStats);
FormatStat(Builder, TEXT("function"), TEXT("functions"), TEXT("remapped"), NumFunctionsRemapped);
FormatStat(Builder, TEXT("scriptstruct"), TEXT("scriptstructs"), TEXT("remapped"), NumScriptStructsRemapped);
}
else
{
Builder << TEXT("No object changes detected");
}
Ar.Logf(ELogVerbosity::Display, TEXT("Reload/Re-instancing Complete: %s"), *Builder);
if (bSendReloadComplete)
{
FCoreUObjectDelegates::ReloadCompleteDelegate.Broadcast(EReloadCompleteReason::None);
}
}
bool FReload::GetEnableReinstancing(bool bHasChanged) const
{
if (bHasChanged && !bEnableReinstancing && !bEnabledMessage)
{
bEnabledMessage = true;
bHasReinstancingOccurred = true;
Ar.Logf(ELogVerbosity::Display, TEXT("Re-instancing has been disabled. Some changes will be ignored."));
}
return bEnableReinstancing;
}
void FReload::Reset()
{
FunctionRemap.Empty();
ReconstructedCDOsMap.Empty();
ReinstancedClasses.Empty();
NewClasses.Empty();
ReinstancedEnums.Empty();
ReinstancedStructs.Empty();
Packages.Empty();
bHasReinstancingOccurred = false;
}
void FReload::UpdateStats(FReinstanceStats& Stats, void* New, void* Old)
{
if (Old == nullptr)
{
++Stats.New;
}
else if (Old != New)
{
++Stats.Changed;
}
else
{
++Stats.Unchanged;
}
}
void FReload::FormatStats(FStringBuilderBase& Out, const TCHAR* Singular, const TCHAR* Plural, const FReinstanceStats& Stats)
{
FormatStat(Out, Singular, Plural, TEXT("new"), Stats.New);
FormatStat(Out, Singular, Plural, TEXT("changed"), Stats.Changed);
FormatStat(Out, Singular, Plural, TEXT("unchanged"), Stats.Unchanged);
}
void FReload::FormatStat(FStringBuilderBase& Out, const TCHAR* Singular, const TCHAR* Plural, const TCHAR* What, int32 Value)
{
if (Value == 0)
{
return;
}
if (Out.Len() != 0)
{
Out << TEXT(", ");
}
Out << Value << TEXT(" ") << (Value > 1 ? Plural : Singular) << TEXT(" ") << What;
}
void FReload::NotifyFunctionRemap(FNativeFuncPtr NewFunctionPointer, FNativeFuncPtr OldFunctionPointer)
{
FNativeFuncPtr OtherNewFunction = FunctionRemap.FindRef(OldFunctionPointer);
check(!OtherNewFunction || OtherNewFunction == NewFunctionPointer);
check(NewFunctionPointer);
check(OldFunctionPointer);
FunctionRemap.Add(OldFunctionPointer, NewFunctionPointer);
}
void FReload::NotifyChange(UClass* New, UClass* Old)
{
UpdateStats(ClassStats, New, Old);
if (New != Old)
{
bHasReinstancingOccurred = true;
}
if (Old != nullptr)
{
// Don't allow re-instancing of UEngine classes
if (!Old->IsChildOf(UEngine::StaticClass()))
{
UClass* NewIfChanged = Old != New ? New : nullptr; // supporting code detects unchanged based on null new pointer
TMap<UClass*, UClass*>& ClassesToReinstance = GetClassesToReinstanceForHotReload();
checkf(!ClassesToReinstance.Contains(Old) || ClassesToReinstance[Old] == NewIfChanged, TEXT("Attempting to reload a class which is already being reloaded as a different class"));
ClassesToReinstance.Add(Old, NewIfChanged);
}
else if (Old != New) // This has changed
{
Ar.Logf(ELogVerbosity::Warning, TEXT("Engine class '%s' has changed but will be ignored for reload"), *New->GetName());
}
}
else if (New != nullptr) // should always be the case, but protect against it
{
NewClasses.Add(New);
}
}
void FReload::NotifyChange(UEnum* New, UEnum* Old)
{
UpdateStats(EnumStats, New, Old);
if (New != Old)
{
bHasReinstancingOccurred = true;
}
if (Old != nullptr)
{
UEnum* NewIfChanged = Old != New ? New : nullptr; // supporting code detects unchanged based on null new pointer
checkf(!ReinstancedEnums.Contains(Old) || ReinstancedEnums[Old] == NewIfChanged, TEXT("Attempting to reload an enumeration which is already being reloaded as a different enumeration"));
ReinstancedEnums.Add(Old, NewIfChanged);
}
}
void FReload::NotifyChange(UScriptStruct* New, UScriptStruct* Old)
{
UpdateStats(StructStats, New, Old);
if (New != Old)
{
bHasReinstancingOccurred = true;
}
if (Old != nullptr)
{
UScriptStruct* NewIfChanged = Old != New ? New : nullptr; // supporting code detects unchanged based on null new pointer
checkf(!ReinstancedStructs.Contains(Old) || ReinstancedStructs[Old] == NewIfChanged, TEXT("Attempting to reload a structure which is already being reloaded as a different structure"));
ReinstancedStructs.Add(Old, NewIfChanged);
}
}
void FReload::NotifyChange(UPackage* New, UPackage* Old)
{
if (Old != nullptr)
{
++PackageStats.Changed;
}
else
{
++PackageStats.New;
}
Packages.Add(New);
}
namespace
{
template<typename T>
void CollectPackages(TSet<UPackage*>& Packages, const TMap<T*, T*>& Reinstances)
{
for (const TPair<T*, T*>& Pair : Reinstances)
{
T* Old = Pair.Key;
T* New = Pair.Value;
Packages.Add(New ? New->GetPackage() : Old->GetPackage());
}
}
}
void FReload::Reinstance()
{
if (Type != EActiveReloadType::Reinstancing)
{
UClass::AssembleReferenceTokenStreams();
}
TMap<UClass*, UClass*>& ClassesToReinstance = GetClassesToReinstanceForHotReload();
// If we have to collect the packages, gather them from the reinstanced objects
if (bCollectPackages)
{
CollectPackages(Packages, ClassesToReinstance);
CollectPackages(Packages, ReinstancedStructs);
CollectPackages(Packages, ReinstancedEnums);
}
// Remap all native functions (and gather scriptstructs)
UPackage* TransientPackage = GetTransientPackage();
TArray<UScriptStruct*> ScriptStructs;
for (FRawObjectIterator It; It; ++It)
{
if (UFunction* Function = Cast<UFunction>(static_cast<UObject*>(It->GetObject())))
{
if (FNativeFuncPtr NewFunction = FunctionRemap.FindRef(Function->GetNativeFunc()))
{
++NumFunctionsRemapped;
Function->SetNativeFunc(NewFunction);
}
}
else if (UScriptStruct* ScriptStruct = Cast<UScriptStruct>(static_cast<UObject*>(It->GetObject())))
{
// IMPORTANT: The order here is very important. There is a chance that GetCppStructOps will fail for
// types that are in a package we don't care about. So we have to filter on the package first.
UPackage* ScriptStructPackage = ScriptStruct->GetPackage();
if (!ScriptStruct->HasAnyFlags(RF_ClassDefaultObject) &&
ScriptStructPackage != TransientPackage &&
Packages.Contains(ScriptStruct->GetPackage()) &&
ScriptStruct->GetCppStructOps())
{
ScriptStructs.Add(ScriptStruct);
}
}
}
// now let's set up the script structs...this relies on super behavior, so null them all, then set them all up. Internally this sets them up hierarchically.
for (UScriptStruct* Script : ScriptStructs)
{
Script->ClearCppStructOps();
}
for (UScriptStruct* Script : ScriptStructs)
{
Script->PrepareCppStructOps();
check(Script->GetCppStructOps());
}
NumScriptStructsRemapped = ScriptStructs.Num();
// Collect all the classes being re-instanced
TSet<UObject*> ReinstancingObjects;
ReinstancingObjects.Reserve(ClassesToReinstance.Num() + ReinstancedStructs.Num() + ReinstancedEnums.Num());
for (const TPair<UClass*, UClass*>& Pair : ClassesToReinstance)
{
ReinstancingObjects.Add(Pair.Key);
}
// Collect all of the blueprint nodes that are getting updated due to enum/struct changes
TMap<UBlueprint*, FBlueprintUpdateInfo> ModifiedBlueprints;
FBlueprintEditorUtils::FOnNodeFoundOrUpdated OnNodeFoundOrUpdated = [&ModifiedBlueprints](UBlueprint* Blueprint, UK2Node* Node)
{
// Blueprint can be nullptr
FBlueprintUpdateInfo& BlueprintUpdateInfo = ModifiedBlueprints.FindOrAdd(Blueprint);
BlueprintUpdateInfo.Nodes.Add(Node);
};
// Update all the structures. We add the unchanging structs to the list to make sure the defaults are updated
TMap<UScriptStruct*, UScriptStruct*> ChangedStructs;
for (const TPair<UScriptStruct*, UScriptStruct*>& Pair : ReinstancedStructs)
{
ReinstancingObjects.Add(Pair.Key);
if (Pair.Value)
{
Pair.Key->StructFlags = EStructFlags(Pair.Key->StructFlags | STRUCT_NewerVersionExists);
ChangedStructs.Emplace(Pair.Key, Pair.Value);
}
else
{
ChangedStructs.Emplace(Pair.Key, Pair.Key);
}
}
FBlueprintEditorUtils::UpdateScriptStructsInNodes(ChangedStructs, OnNodeFoundOrUpdated);
// Update all the enumeration nodes
TMap<UEnum*, UEnum*> ChangedEnums;
for (const TPair<UEnum*, UEnum*>& Pair : ReinstancedEnums)
{
ReinstancingObjects.Add(Pair.Key);
if (Pair.Value)
{
Pair.Key->SetEnumFlags(EEnumFlags::NewerVersionExists);
ChangedEnums.Emplace(Pair.Key, Pair.Value);
}
}
FBlueprintEditorUtils::UpdateEnumsInNodes(ChangedEnums, OnNodeFoundOrUpdated);
// Update all the nodes before we could possibly recompile
for (TPair<UBlueprint*, FBlueprintUpdateInfo>& KVP : ModifiedBlueprints) //-V1078
{
UBlueprint* Blueprint = KVP.Key;
FBlueprintUpdateInfo& Info = KVP.Value;
for (UK2Node* Node : Info.Nodes)
{
FBlueprintEditorUtils::RecombineNestedSubPins(Node);
}
// We must reconstruct the node first other wise some pins might not be
// in a good state for the recompile
for (UK2Node* Node : Info.Nodes)
{
Node->ReconstructNode();
}
}
// Re-instance the classes
TSet<UBlueprint*> CompiledBlueprints;
UE::Reload::Private::FReloadClassHelper rch(Ar, ReinstancingObjects, CompiledBlueprints, ReconstructedCDOsMap, ReinstancedCDOsMap);
rch.ReinstanceClasses(ClassesToReinstance);
// Recompile blueprints if they haven't already been recompiled)
for (TPair<UBlueprint*, FBlueprintUpdateInfo>& KVP : ModifiedBlueprints) //-V1078
{
UBlueprint* Blueprint = KVP.Key;
FBlueprintUpdateInfo& Info = KVP.Value;
if (Blueprint && !CompiledBlueprints.Contains(Blueprint))
{
EBlueprintCompileOptions Options = EBlueprintCompileOptions::SkipGarbageCollection;
FKismetEditorUtilities::CompileBlueprint(Blueprint, Options);
}
}
ReinstancedClasses = MoveTemp(ClassesToReinstance);
FCoreUObjectDelegates::ReloadReinstancingCompleteDelegate.Broadcast();
}
UObject* FReload::GetReinstancedCDO(UObject* CDO)
{
return const_cast<UObject*>(GetReinstancedCDO(const_cast<const UObject*>(CDO)));
}
const UObject* FReload::GetReinstancedCDO(const UObject* CDO)
{
UObject* const* NewCDO = ReconstructedCDOsMap.Find(CDO);
if (NewCDO != nullptr)
{
return *NewCDO;
}
NewCDO = ReinstancedCDOsMap.Find(CDO);
if (NewCDO != nullptr)
{
return *NewCDO;
}
return CDO;
}
void FReload::Finalize(bool bRunGC)
{
FAssetCompilingManager::Get().FinishAllCompilation();
// Make sure new classes have the token stream assembled
UClass::AssembleReferenceTokenStreams();
ReplaceReferencesToReconstructedCDOs();
// Force GC to collect reinstanced objects
if (bRunGC)
{
// Make sure the GIsInitialLoad flag is false. Otherwise GC does nothing
TGuardValue<bool> GuardIsInitialLoad(GIsInitialLoad, false);
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, true);
}
}
void FReload::ReplaceReferencesToReconstructedCDOs()
{
if (ReconstructedCDOsMap.Num() == 0)
{
return;
}
// Thread pool manager. We need new thread pool with increased
// amount of stack size. Standard GThreadPool was encountering
// stack overflow error during serialization.
static struct FReplaceReferencesThreadPool
{
FReplaceReferencesThreadPool()
{
Pool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
verify(Pool->Create(NumThreadsInThreadPool, 256 * 1024));
}
~FReplaceReferencesThreadPool()
{
Pool->Destroy();
}
FQueuedThreadPool* GetPool() { return Pool; }
private:
FQueuedThreadPool* Pool;
} ThreadPoolManager;
// Async task to enable multithreaded CDOs reference search.
class FFindRefTask : public FNonAbandonableTask
{
public:
explicit FFindRefTask(const TMap<UObject*, UObject*>& InReconstructedCDOsMap, int32 ReserveElements)
: ReconstructedCDOsMap(InReconstructedCDOsMap)
{
ObjectsArray.Reserve(ReserveElements);
}
void DoWork()
{
for (UObject* Object : ObjectsArray)
{
class FReplaceCDOReferencesArchive : public FArchiveUObject
{
public:
FReplaceCDOReferencesArchive(UObject* InPotentialReferencer, const TMap<UObject*, UObject*>& InReconstructedCDOsMap)
: ReconstructedCDOsMap(InReconstructedCDOsMap)
, PotentialReferencer(InPotentialReferencer)
{
ArIsObjectReferenceCollector = true;
ArIgnoreOuterRef = true;
ArShouldSkipBulkData = true;
}
virtual FString GetArchiveName() const override
{
return TEXT("FReplaceCDOReferencesArchive");
}
FArchive& operator<<(UObject*& ObjRef)
{
UObject* Obj = ObjRef;
if (Obj && Obj != PotentialReferencer)
{
if (UObject* const* FoundObj = ReconstructedCDOsMap.Find(Obj))
{
ObjRef = *FoundObj;
}
}
return *this;
}
virtual bool ShouldSkipProperty(const FProperty* InProperty) const
{
return InProperty->GetClass()->HasAnyCastFlags(CASTCLASS_FDelegateProperty | CASTCLASS_FMulticastDelegateProperty | CASTCLASS_FMulticastInlineDelegateProperty | CASTCLASS_FMulticastSparseDelegateProperty);
}
const TMap<UObject*, UObject*>& ReconstructedCDOsMap;
UObject* PotentialReferencer;
};
FReplaceCDOReferencesArchive FindRefsArchive(Object, ReconstructedCDOsMap);
Object->Serialize(FindRefsArchive);
}
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FFindRefTask, STATGROUP_ThreadPoolAsyncTasks);
}
TArray<UObject*> ObjectsArray;
private:
const TMap<UObject*, UObject*>& ReconstructedCDOsMap;
};
const int32 NumberOfThreads = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
const int32 NumObjects = GUObjectArray.GetObjectArrayNum();
const int32 ObjectsPerTask = FMath::CeilToInt((float)NumObjects / NumberOfThreads);
// Create tasks.
TArray<FAsyncTask<FFindRefTask>> Tasks;
Tasks.Reserve(NumberOfThreads);
for (int32 TaskId = 0; TaskId < NumberOfThreads; ++TaskId)
{
Tasks.Emplace(ReconstructedCDOsMap, ObjectsPerTask);
}
// Distribute objects uniformly between tasks.
int32 CurrentTaskId = 0;
for (FThreadSafeObjectIterator ObjIter; ObjIter; ++ObjIter)
{
UObject* CurObject = *ObjIter;
if (!IsValidChecked(CurObject))
{
continue;
}
Tasks[CurrentTaskId].GetTask().ObjectsArray.Add(CurObject);
CurrentTaskId = (CurrentTaskId + 1) % NumberOfThreads;
}
// Run async tasks in worker threads.
for (FAsyncTask<FFindRefTask>& Task : Tasks)
{
Task.StartBackgroundTask(ThreadPoolManager.GetPool());
}
// Wait until tasks are finished
for (FAsyncTask<FFindRefTask>& AsyncTask : Tasks)
{
AsyncTask.EnsureCompletion();
}
}