Files
UnrealEngineUWP/Engine/Source/Runtime/Engine/Private/StreamableManager.cpp
justin marcus d535acd36e Only check bStartStalledHandles once.
#preflight 6410b8895819afacafbd380e

[CL 24643926 by justin marcus in ue5-main branch]
2023-03-14 16:48:19 -04:00

1927 lines
54 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Engine/StreamableManager.h"
#include "UObject/ObjectRedirector.h"
#include "UObject/Package.h"
#include "Misc/PackageName.h"
#include "UObject/UObjectThreadContext.h"
#include "HAL/IConsoleManager.h"
#include "Tickable.h"
#include "Serialization/LoadTimeTrace.h"
DEFINE_LOG_CATEGORY_STATIC(LogStreamableManager, Log, All);
// Default to 1 frame, this will cause the delegates to go off on the next tick to avoid recursion issues. Set higher to fake disk lag
static int32 GStreamableDelegateDelayFrames = 1;
static FAutoConsoleVariableRef CVarStreamableDelegateDelayFrames(
TEXT("s.StreamableDelegateDelayFrames"),
GStreamableDelegateDelayFrames,
TEXT("Number of frames to delay StreamableManager delegates "),
ECVF_Default
);
const FString FStreamableHandle::HandleDebugName_Preloading = FString(TEXT("Preloading"));
const FString FStreamableHandle::HandleDebugName_AssetList = FString(TEXT("LoadAssetList"));
const FString FStreamableHandle::HandleDebugName_CombinedHandle = FString(TEXT("CreateCombinedHandle"));
/** Helper class that defers streamable manager delegates until the next frame */
class FStreamableDelegateDelayHelper : public FTickableGameObject
{
public:
/** Adds a delegate to deferred list */
void AddDelegate(const FStreamableDelegate& Delegate, const FStreamableDelegate& CancelDelegate, TSharedPtr<FStreamableHandle> AssociatedHandle)
{
FPendingDelegate* PendingDelegate = new FPendingDelegate(Delegate, CancelDelegate, AssociatedHandle);
{
FScopeLock Lock(&DataLock);
FPendingDelegateList& DelegatesForHandle = PendingDelegatesByHandle.FindOrAdd(AssociatedHandle);
FPendingDelegateList::AddTail(PendingDelegate, PendingDelegates, DelegatesForHandle);
}
}
/** Cancels delegate for handle, this will either delete the delegate or replace with the cancel delegate */
void CancelDelegatesForHandle(TSharedPtr<FStreamableHandle> AssociatedHandle)
{
FPendingDelegateList PendingDelegatesToDelete;
{
FScopeLock _(&DataLock);
FPendingDelegateList* DelegatesForHandle = PendingDelegatesByHandle.Find(AssociatedHandle);
if (!DelegatesForHandle)
{
return;
}
FPendingDelegate* CurrentNode = DelegatesForHandle->Head;
while (CurrentNode)
{
FPendingDelegate* NextNode = CurrentNode->NextByHandle;
if (CurrentNode->CancelDelegate.IsBound())
{
// Replace with cancel delegate
CurrentNode->Delegate = CurrentNode->CancelDelegate;
CurrentNode->CancelDelegate.Unbind();
}
else
{
// Remove entirely
FPendingDelegateList::Unlink(CurrentNode, PendingDelegates, *DelegatesForHandle);
if (PendingDelegatesToDelete.Tail)
{
PendingDelegatesToDelete.Tail->Next = CurrentNode;
PendingDelegatesToDelete.Tail = CurrentNode;
}
else
{
PendingDelegatesToDelete.Head = PendingDelegatesToDelete.Tail = CurrentNode;
}
CurrentNode->Next = nullptr;
}
CurrentNode = NextNode;
}
if (!DelegatesForHandle->Head)
{
RemovePendingDelegateInternal(AssociatedHandle);
}
}
FPendingDelegate* NodeToDelete = PendingDelegatesToDelete.Head;
while (NodeToDelete)
{
FPendingDelegate* NextNodeToDelete = NodeToDelete->Next;
delete NodeToDelete;
NodeToDelete = NextNodeToDelete;
}
}
/** Calls all delegates, call from synchronous flushes */
void FlushDelegates()
{
while (PendingDelegates.Head)
{
Tick(0.0f);
}
}
// FTickableGameObject interface
void Tick(float DeltaTime) override
{
if (!PendingDelegates.Head)
{
return;
}
FPendingDelegateList DelegatesToCall;
{
FScopeLock Lock(&DataLock);
FPendingDelegate* CurrentNode = PendingDelegates.Head;
while (CurrentNode)
{
FPendingDelegate* NextNode = CurrentNode->Next;
if (--CurrentNode->DelayFrames <= 0)
{
// Add to call array and remove from tracking one
FPendingDelegateList* DelegatesForHandle = PendingDelegatesByHandle.Find(CurrentNode->RelatedHandle);
check(DelegatesForHandle);
FPendingDelegateList::Unlink(CurrentNode, PendingDelegates, *DelegatesForHandle);
if (DelegatesToCall.Tail)
{
DelegatesToCall.Tail->Next = CurrentNode;
DelegatesToCall.Tail = CurrentNode;
}
else
{
DelegatesToCall.Head = DelegatesToCall.Tail = CurrentNode;
}
if (!DelegatesForHandle->Head)
{
RemovePendingDelegateInternal(CurrentNode->RelatedHandle);
}
}
CurrentNode = NextNode;
}
}
FPendingDelegate* DelegateToCall = DelegatesToCall.Head;
while (DelegateToCall)
{
FPendingDelegate* NextDelegateToCall = DelegateToCall->Next;
DelegateToCall->Delegate.ExecuteIfBound();
delete DelegateToCall;
DelegateToCall = NextDelegateToCall;
}
}
virtual ETickableTickType GetTickableTickType() const override
{
return ETickableTickType::Always;
}
virtual bool IsTickableWhenPaused() const override
{
return true;
}
virtual bool IsTickableInEditor() const
{
return true;
}
virtual TStatId GetStatId() const override
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FStreamableDelegateDelayHelper, STATGROUP_Tickables);
}
private:
struct FPendingDelegate
{
FPendingDelegate* Prev;
FPendingDelegate* Next;
FPendingDelegate* PrevByHandle;
FPendingDelegate* NextByHandle;
/** Delegate to call when frames are up */
FStreamableDelegate Delegate;
/** Delegate to call if this gets cancelled early */
FStreamableDelegate CancelDelegate;
/** Handle related to delegates, needs to keep these around to avoid things GCing before the user callback goes off. This may be null */
TSharedPtr<FStreamableHandle> RelatedHandle;
/** Frames left to delay */
int32 DelayFrames;
FPendingDelegate(const FStreamableDelegate& InDelegate, const FStreamableDelegate& InCancelDelegate, TSharedPtr<FStreamableHandle> InHandle)
: Delegate(InDelegate)
, CancelDelegate(InCancelDelegate)
, RelatedHandle(InHandle)
, DelayFrames(GStreamableDelegateDelayFrames)
{}
};
struct FPendingDelegateList
{
FPendingDelegate* Head = nullptr;
FPendingDelegate* Tail = nullptr;
static void Unlink(FPendingDelegate* PendingDelegate, FPendingDelegateList& PendingDelegates, FPendingDelegateList& PendingDelegatesByHandle)
{
if (PendingDelegate->Prev)
{
PendingDelegate->Prev->Next = PendingDelegate->Next;
}
else
{
check(PendingDelegate == PendingDelegates.Head);
PendingDelegates.Head = PendingDelegate->Next;
}
if (PendingDelegate->Next)
{
PendingDelegate->Next->Prev = PendingDelegate->Prev;
}
else
{
check(PendingDelegate == PendingDelegates.Tail);
PendingDelegates.Tail = PendingDelegate->Prev;
}
PendingDelegate->Next = PendingDelegate->Prev = nullptr;
if (PendingDelegate->PrevByHandle)
{
PendingDelegate->PrevByHandle->NextByHandle = PendingDelegate->NextByHandle;
}
else
{
check(PendingDelegate == PendingDelegatesByHandle.Head);
PendingDelegatesByHandle.Head = PendingDelegate->NextByHandle;
}
if (PendingDelegate->NextByHandle)
{
PendingDelegate->NextByHandle->PrevByHandle = PendingDelegate->PrevByHandle;
}
else
{
check(PendingDelegate == PendingDelegatesByHandle.Tail);
PendingDelegatesByHandle.Tail = PendingDelegate->PrevByHandle;
}
PendingDelegate->NextByHandle = PendingDelegate->PrevByHandle = nullptr;
}
static void AddTail(FPendingDelegate* PendingDelegate, FPendingDelegateList& PendingDelegates, FPendingDelegateList& PendingDelegatesByHandle)
{
if (PendingDelegates.Tail)
{
PendingDelegates.Tail->Next = PendingDelegate;
PendingDelegate->Prev = PendingDelegates.Tail;
PendingDelegate->Next = nullptr;
PendingDelegates.Tail = PendingDelegate;
}
else
{
check(!PendingDelegates.Head);
PendingDelegates.Head = PendingDelegates.Tail = PendingDelegate;
PendingDelegate->Next = PendingDelegate->Prev = nullptr;
}
if (PendingDelegatesByHandle.Tail)
{
PendingDelegatesByHandle.Tail->NextByHandle = PendingDelegate;
PendingDelegate->PrevByHandle = PendingDelegatesByHandle.Tail;
PendingDelegate->NextByHandle = nullptr;
PendingDelegatesByHandle.Tail = PendingDelegate;
}
else
{
check(!PendingDelegatesByHandle.Head);
PendingDelegatesByHandle.Head = PendingDelegatesByHandle.Tail = PendingDelegate;
PendingDelegate->NextByHandle = PendingDelegate->PrevByHandle = nullptr;
}
}
};
inline void RemovePendingDelegateInternal(TSharedPtr<FStreamableHandle> Handle)
{
// requires DataLock by caller
PendingDelegatesByHandle.Remove(Handle);
if (PendingDelegatesByHandle.IsEmpty())
{
// release potentially big allocation after huge batches of streaming requests
PendingDelegatesByHandle.Empty(128);
}
}
FPendingDelegateList PendingDelegates;
TMap<TSharedPtr<FStreamableHandle>, FPendingDelegateList> PendingDelegatesByHandle;
FCriticalSection DataLock;
};
static FStreamableDelegateDelayHelper* StreamableDelegateDelayHelper = nullptr;
TStreamableHandleContextDataTypeID FStreamableHandleContextDataBase::AllocateClassTypeId()
{
static std::atomic<TStreamableHandleContextDataTypeID> TypeNum{0};
TStreamableHandleContextDataTypeID Result = TypeNum++;
checkf(Result != TStreamableHandleContextDataTypeIDInvalid, TEXT("Overflow in TypeNum: too many TStreamableHandleContextData subclasses. Change the TStreamableHandleContextDataTypeID typedef if more subclasses are needed."));
return Result;
}
bool FStreamableHandle::BindCompleteDelegate(FStreamableDelegate NewDelegate)
{
if (!IsLoadingInProgress())
{
// Too late!
return false;
}
CompleteDelegate = NewDelegate;
return true;
}
bool FStreamableHandle::BindCancelDelegate(FStreamableDelegate NewDelegate)
{
if (!IsLoadingInProgress())
{
// Too late!
return false;
}
CancelDelegate = NewDelegate;
return true;
}
bool FStreamableHandle::BindUpdateDelegate(FStreamableUpdateDelegate NewDelegate)
{
if (!IsLoadingInProgress())
{
// Too late!
return false;
}
UpdateDelegate = NewDelegate;
return true;
}
EAsyncPackageState::Type FStreamableHandle::WaitUntilComplete(float Timeout, bool bStartStalledHandles)
{
if (HasLoadCompleted())
{
return EAsyncPackageState::Complete;
}
// We need to recursively start any stalled handles
if (bStartStalledHandles)
{
TArray<TSharedRef<FStreamableHandle>> HandlesToStart;
HandlesToStart.Add(AsShared());
for (int32 i = 0; i < HandlesToStart.Num(); i++)
{
TSharedRef<FStreamableHandle> Handle = HandlesToStart[i];
if (Handle->IsStalled())
{
// If we were stalled, start us now to avoid deadlocks
UE_LOG(LogStreamableManager, Warning, TEXT("FStreamableHandle::WaitUntilComplete called on stalled handle %s, forcing load even though resources may not have been acquired yet"), *Handle->GetDebugName());
Handle->StartStalledHandle();
}
for (const TSharedPtr<FStreamableHandle>& ChildHandle : Handle->ChildHandles)
{
if (ChildHandle.IsValid())
{
HandlesToStart.Add(ChildHandle.ToSharedRef());
}
}
}
}
// Finish when all handles are completed or stalled. If we started stalled above then there will be no stalled handles
EAsyncPackageState::Type State = ProcessAsyncLoadingUntilComplete([this]() { return HasLoadCompletedOrStalled(); }, Timeout);
if (State == EAsyncPackageState::Complete)
{
ensureMsgf(HasLoadCompletedOrStalled() || WasCanceled(), TEXT("WaitUntilComplete failed for streamable handle %s, async loading is done but handle is not complete"), *GetDebugName());
}
return State;
}
bool FStreamableHandle::HasLoadCompletedOrStalled() const
{
// Check this handle
if (!IsCombinedHandle() && !HasLoadCompleted() && !IsStalled())
{
return false;
}
// Check children recursively
for (const TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
if (ChildHandle.IsValid() && !ChildHandle->HasLoadCompletedOrStalled())
{
return false;
}
}
return true;
}
bool FStreamableHandle::IsHandleNameEmptyOrDefault() const
{
// empty or "None"
if (DebugName.IsEmpty() || DebugName.Equals(FName(NAME_None).ToString()))
{
return true;
}
// a default value...
if (DebugName == HandleDebugName_AssetList || DebugName == HandleDebugName_CombinedHandle)
{
return true;
}
// a default generated debug name
else if (DebugName.StartsWith(HandleDebugName_Preloading))
{
return true;
}
return false;
}
void FStreamableHandle::SetDebugNameIfEmptyOrDefault(const FString& NewName)
{
if (IsHandleNameEmptyOrDefault())
{
DebugName = NewName;
}
}
void FStreamableHandle::GetRequestedAssets(TArray<FSoftObjectPath>& AssetList, bool bIncludeChildren /*= true*/) const
{
AssetList = RequestedAssets;
// Check child handles
if (bIncludeChildren)
{
for (const TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
TArray<FSoftObjectPath> ChildAssetList;
ChildHandle->GetRequestedAssets(ChildAssetList);
for (const FSoftObjectPath& ChildRef : ChildAssetList)
{
AssetList.AddUnique(ChildRef);
}
}
}
}
UObject* FStreamableHandle::GetLoadedAsset() const
{
TArray<UObject *> LoadedAssets;
GetLoadedAssets(LoadedAssets);
if (LoadedAssets.Num() > 0)
{
return LoadedAssets[0];
}
return nullptr;
}
void FStreamableHandle::GetLoadedAssets(TArray<UObject *>& LoadedAssets) const
{
ForEachLoadedAsset([&LoadedAssets](UObject* LoadedAsset)
{
LoadedAssets.Add(LoadedAsset);
});
}
void FStreamableHandle::GetLoadedCount(int32& LoadedCount, int32& RequestedCount) const
{
RequestedCount = RequestedAssets.Num();
LoadedCount = RequestedCount - StreamablesLoading;
// Check child handles
for (const TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
int32 ChildRequestedCount = 0;
int32 ChildLoadedCount = 0;
ChildHandle->GetLoadedCount(ChildLoadedCount, ChildRequestedCount);
LoadedCount += ChildLoadedCount;
RequestedCount += ChildRequestedCount;
}
}
float FStreamableHandle::GetProgress() const
{
if (HasLoadCompleted())
{
return 1.0f;
}
int32 Loaded, Total;
GetLoadedCount(Loaded, Total);
return (float)Loaded / Total;
}
FStreamableManager* FStreamableHandle::GetOwningManager() const
{
check(IsInGameThread());
if (IsActive())
{
return OwningManager;
}
return nullptr;
}
void FStreamableHandle::CancelHandle()
{
check(IsInGameThread());
if (bCanceled || !OwningManager)
{
// Too late to cancel it
return;
}
TSharedRef<FStreamableHandle> SharedThis = AsShared();
if (bLoadCompleted || bReleased)
{
// Cancel if it's in the pending queue
if (StreamableDelegateDelayHelper)
{
StreamableDelegateDelayHelper->CancelDelegatesForHandle(SharedThis);
}
if (!bReleased)
{
ReleaseHandle();
}
bCanceled = true;
NotifyParentsOfCancellation();
return;
}
bCanceled = true;
NotifyParentsOfCancellation();
ExecuteDelegate(CancelDelegate, SharedThis);
UnbindDelegates();
// Remove from referenced list. If it is stalled then it won't have been registered with
// the manager yet
if (!bStalled)
{
for (const FSoftObjectPath& AssetRef : RequestedAssets)
{
OwningManager->RemoveReferencedAsset(AssetRef, SharedThis);
}
}
// Remove from explicit list
OwningManager->ManagedActiveHandles.Remove(SharedThis);
// Remove child handles
for (TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
ChildHandle->ParentHandles.Remove(SharedThis);
}
ChildHandles.Empty();
CompletedChildCount = 0;
CanceledChildCount = 0;
OwningManager = nullptr;
if (ParentHandles.Num() > 0)
{
// Update any meta handles that are still active. Copy the array first as elements may be removed from original while iterating
TArray<TWeakPtr<FStreamableHandle>> ParentHandlesCopy = ParentHandles;
for (TWeakPtr<FStreamableHandle>& WeakHandle : ParentHandlesCopy)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
Handle->UpdateCombinedHandle();
}
}
}
}
void FStreamableHandle::ReleaseHandle()
{
check(IsInGameThread());
if (bReleased || bCanceled)
{
// Too late to release it
return;
}
check(OwningManager);
if (bLoadCompleted)
{
bReleased = true;
TSharedRef<FStreamableHandle> SharedThis = AsShared();
// Remove from referenced list
for (const FSoftObjectPath& AssetRef : RequestedAssets)
{
OwningManager->RemoveReferencedAsset(AssetRef, SharedThis);
}
// Remove from explicit list
OwningManager->ManagedActiveHandles.Remove(SharedThis);
// Remove child handles
for (TSharedPtr<FStreamableHandle> ChildHandle : ChildHandles)
{
ChildHandle->ParentHandles.Remove(SharedThis);
}
ChildHandles.Empty();
CompletedChildCount = 0;
CanceledChildCount = 0;
OwningManager = nullptr;
}
else
{
// Set to release on complete
bReleaseWhenLoaded = true;
}
}
void FStreamableHandle::StartStalledHandle()
{
if (!bStalled || !IsActive())
{
// Cannot start
return;
}
check(OwningManager);
bStalled = false;
OwningManager->StartHandleRequests(AsShared());
}
bool FStreamableHandle::HasCompleteDelegate() const
{
return CompleteDelegate.IsBound();
}
bool FStreamableHandle::HasCancelDelegate() const
{
return CancelDelegate.IsBound();
}
bool FStreamableHandle::HasUpdateDelegate() const
{
return UpdateDelegate.IsBound();
}
static void RemoveActiveHandle(FStreamable& Streamable, FStreamableHandle& Handle);
FStreamableHandle::~FStreamableHandle()
{
checkf(IsInGameThread(), TEXT("Streamable handles aren't thread-safe and must be destroyed on the game thread"));
if (IsActive())
{
if (!bStalled)
{
for (const FSoftObjectPath& RequestedAsset : RequestedAssets)
{
if (FStreamable* Streamable = OwningManager->FindStreamable(RequestedAsset))
{
RemoveActiveHandle(*Streamable, *this);
}
}
}
bReleased = true;
OwningManager = nullptr;
// The weak pointers in FStreamable will be nulled, but they're fixed on next GC, and actively canceling is not safe as we're halfway destroyed
}
}
void FStreamableHandle::CompleteLoad()
{
// Only complete if it's still active
if (IsActive())
{
bLoadCompleted = true;
ExecuteDelegate(CompleteDelegate, AsShared(), CancelDelegate);
UnbindDelegates();
NotifyParentsOfCompletion();
if (ParentHandles.Num() > 0)
{
// Update any meta handles that are still active. Copy the array first as elements may be removed from original while iterating
TArray<TWeakPtr<FStreamableHandle>> ParentHandlesCopy = ParentHandles;
for (TWeakPtr<FStreamableHandle>& WeakHandle : ParentHandlesCopy)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
Handle->UpdateCombinedHandle();
}
}
}
}
}
void FStreamableHandle::NotifyParentsOfCompletion()
{
if (ParentHandles.Num() > 0)
{
// Update any meta handles that are still active. Copy the array first as elements may be removed from original while iterating
TArray<TWeakPtr<FStreamableHandle>> ParentHandlesCopy = ParentHandles;
for (TWeakPtr<FStreamableHandle>& WeakHandle : ParentHandlesCopy)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
++Handle->CompletedChildCount;
}
}
}
}
void FStreamableHandle::NotifyParentsOfCancellation()
{
if (ParentHandles.Num() > 0)
{
// Update any meta handles that are still active. Copy the array first as elements may be removed from original while iterating
TArray<TWeakPtr<FStreamableHandle>> ParentHandlesCopy = ParentHandles;
for (TWeakPtr<FStreamableHandle>& WeakHandle : ParentHandlesCopy)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
++Handle->CanceledChildCount;
}
}
}
}
void FStreamableHandle::UpdateCombinedHandle()
{
if (!IsActive())
{
return;
}
if (!ensure(IsCombinedHandle()))
{
return;
}
if (CompletedChildCount + CanceledChildCount < ChildHandles.Num())
{
return;
}
// Check all our children, complete if done
bool bAllCompleted = true;
bool bAllCanceled = true;
for (const TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
bAllCompleted = bAllCompleted && !ChildHandle->IsLoadingInProgress();
bAllCanceled = bAllCanceled && ChildHandle->WasCanceled();
if (!bAllCompleted && !bAllCanceled)
{
return;
}
}
// If all our sub handles were canceled, cancel us. Otherwise complete us if at least one was completed and there are none in progress
if (bAllCanceled)
{
if (OwningManager)
{
OwningManager->PendingCombinedHandles.Remove(AsShared());
}
CancelHandle();
}
else if (bAllCompleted)
{
if (OwningManager)
{
OwningManager->PendingCombinedHandles.Remove(AsShared());
}
CompleteLoad();
if (bReleaseWhenLoaded)
{
ReleaseHandle();
}
}
}
void FStreamableHandle::CallUpdateDelegate()
{
UpdateDelegate.ExecuteIfBound(AsShared());
// Update any meta handles that are still active
for (TWeakPtr<FStreamableHandle>& WeakHandle : ParentHandles)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
Handle->CallUpdateDelegate();
}
}
}
void FStreamableHandle::UnbindDelegates()
{
CancelDelegate.Unbind();
UpdateDelegate.Unbind();
CompleteDelegate.Unbind();
}
void FStreamableHandle::AsyncLoadCallbackWrapper(const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result, FSoftObjectPath TargetName)
{
check(IsInGameThread());
// Needed so we can bind with a shared pointer for safety
if (OwningManager)
{
OwningManager->AsyncLoadCallback(TargetName, Package);
if (!HasLoadCompleted())
{
CallUpdateDelegate();
}
}
else if (!bCanceled)
{
UE_LOG(LogStreamableManager, Verbose, TEXT("FStreamableHandle::AsyncLoadCallbackWrapper called on request %s with result %d with no active manager!"), *DebugName, (int32)Result);
}
}
void FStreamableHandle::ExecuteDelegate(const FStreamableDelegate& Delegate, TSharedPtr<FStreamableHandle> AssociatedHandle, const FStreamableDelegate& CancelDelegate)
{
if (Delegate.IsBound())
{
if (GStreamableDelegateDelayFrames == 0)
{
// Execute it immediately
Delegate.Execute();
}
else
{
// Add to execution queue for next tick
if (!StreamableDelegateDelayHelper)
{
StreamableDelegateDelayHelper = new FStreamableDelegateDelayHelper;
}
StreamableDelegateDelayHelper->AddDelegate(Delegate, CancelDelegate, AssociatedHandle);
}
}
}
TSharedPtr<FStreamableHandle> FStreamableHandle::FindMatchingHandle(TFunction<bool(const FStreamableHandle&)> Predicate) const
{
if (Predicate(*this))
{
return ConstCastSharedPtr<FStreamableHandle, const FStreamableHandle, ESPMode::ThreadSafe>(AsShared());
}
for (const TSharedPtr<FStreamableHandle>& ChildHandle : ChildHandles)
{
if (TSharedPtr<FStreamableHandle> MatchingHandleFromChildren = ChildHandle ? ChildHandle->FindMatchingHandle(Predicate) : nullptr)
{
return MatchingHandleFromChildren;
}
}
return nullptr;
}
TSharedPtr<FStreamableHandle> FStreamableHandle::CreateCombinedHandle(const TConstArrayView<TSharedPtr<FStreamableHandle>>& OtherHandles)
{
static const EStreamableManagerCombinedHandleOptions MergeOptions
= EStreamableManagerCombinedHandleOptions::MergeDebugNames | EStreamableManagerCombinedHandleOptions::SkipNulls | EStreamableManagerCombinedHandleOptions::RedirectParents;
TSharedPtr<FStreamableHandle> ThisAsShared{AsShared()};
if (FStreamableManager* Manager = GetOwningManager())
{
TArray<TSharedPtr<FStreamableHandle>> HandlesToUse{OtherHandles};
HandlesToUse.AddUnique(ThisAsShared);
return Manager->CreateCombinedHandle(HandlesToUse, HandleDebugName_CombinedHandle, MergeOptions);
}
return nullptr;
}
TSharedPtr<FStreamableHandle> FStreamableHandle::GetOutermostHandle()
{
if (ParentHandles.Num())
{
// have at least one parent handle.
if (ParentHandles.Num() == 1)
{
// can just return the outermost of our one parent
return ParentHandles[0].Pin()->GetOutermostHandle();
}
TArray<TWeakPtr<FStreamableHandle>> IterationArray;
IterationArray = ParentHandles;
TArray<TWeakPtr<FStreamableHandle>> NextItrArray;
bool bFoundParents = true;
while (bFoundParents)
{
for (const TWeakPtr<FStreamableHandle>& WeakHandle : IterationArray)
{
TSharedPtr<FStreamableHandle> Handle = WeakHandle.Pin();
if (Handle.IsValid())
{
// Append does the right thing with the allocation already, no need to handle it ourselves
NextItrArray.Append(Handle->ParentHandles);
}
}
// we didn't find parents if none of our handles we just checked had parent handles
bFoundParents = NextItrArray.Num() > 0;
if (!bFoundParents)
{
// we can't be in the loop if this array is empty...
// possible this can actually be more than one handle at this point - that's okay!
return IterationArray[0].Pin();
}
IterationArray = NextItrArray;
NextItrArray.Reset();
}
}
// weren't able to resolve the maximum outermost (likely due to no parent)
return AsShared();
}
/** Internal object, one of these per object paths managed by this system */
struct FStreamable
{
/** Hard GC pointer to object */
UObject* Target = nullptr;
/** Live handles keeping this alive. Handles may be duplicated. */
TArray<FStreamableHandle*> ActiveHandles;
/** Handles waiting for this to load. Handles may be duplicated with redirectors. */
TArray< TSharedRef< FStreamableHandle> > LoadingHandles;
/** If this object is currently being loaded */
bool bAsyncLoadRequestOutstanding = false;
/** If this object failed to load, don't try again */
bool bLoadFailed = false;
void FreeHandles()
{
// Clear the loading handles
for (TSharedRef<FStreamableHandle>& LoadingHandle : LoadingHandles)
{
LoadingHandle->StreamablesLoading--;
}
LoadingHandles.Empty();
// Cancel active handles, this list includes the loading handles
while (ActiveHandles.Num() > 0)
{
FStreamableHandle* ActiveHandle = ActiveHandles.Pop(/* shrink */ false);
if (!ActiveHandle->bCanceled)
{
// Full cancel isn't safe any more
ActiveHandle->bCanceled = true;
ActiveHandle->OwningManager = nullptr;
if (ActiveHandle->bReleased)
{
ActiveHandle->NotifyParentsOfCancellation();
}
else
{
// Keep handle alive to stop the cancel callback from dropping the last reference
TSharedPtr<FStreamableHandle> SharedHandle = ActiveHandle->AsShared();
FStreamableHandle::ExecuteDelegate(ActiveHandle->CancelDelegate, SharedHandle);
ActiveHandle->UnbindDelegates();
ActiveHandle->NotifyParentsOfCancellation();
}
}
}
}
void AddLoadingRequest(TSharedRef<FStreamableHandle>& NewRequest)
{
// With redirectors we can end up adding the same handle multiple times, this is unusual but supported
ActiveHandles.Add(&NewRequest.Get());
LoadingHandles.Add(NewRequest);
NewRequest->StreamablesLoading++;
}
};
void RemoveActiveHandle(FStreamable& Streamable, FStreamableHandle& Handle)
{
TArray<FStreamableHandle*>& ActiveHandles = Streamable.ActiveHandles;
for (int32 Idx = Streamable.ActiveHandles.Num() - 1; Idx >= 0; --Idx)
{
if (ActiveHandles[Idx] == &Handle)
{
ActiveHandles.RemoveAtSwap(Idx, 1, /* shrink */ false);
}
}
}
FStreamableManager::FStreamableManager()
: FGCObject(EFlags::AddStableNativeReferencesOnly)
{
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().AddRaw(this, &FStreamableManager::OnPreGarbageCollect);
bForceSynchronousLoads = false;
ManagerName = TEXT("StreamableManager");
}
FStreamableManager::~FStreamableManager()
{
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().RemoveAll(this);
for (const TPair<FSoftObjectPath, FStreamable*>& Pair : StreamableItems)
{
Pair.Value->FreeHandles();
}
TStreamableMap TempStreamables;
Swap(TempStreamables, StreamableItems);
for (const TPair<FSoftObjectPath, FStreamable*>& TempPair : TempStreamables)
{
delete TempPair.Value;
}
}
void FStreamableManager::OnPreGarbageCollect()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FStreamableManager::OnPreGarbageCollect);
// Find streamables without active handles
TBitArray<> InactiveStreamables;
InactiveStreamables.SetNumUninitialized(StreamableItems.Num());
int32 Idx = 0;
for (const TPair<FSoftObjectPath, FStreamable*>& Pair : StreamableItems)
{
InactiveStreamables[Idx++] = Pair.Value->ActiveHandles.IsEmpty();
}
int32 NumInactive = InactiveStreamables.CountSetBits();
if (NumInactive == 0)
{
return;
}
// Remove redirects
if (StreamableRedirects.Num() > 0)
{
TSet<FSoftObjectPath> RedirectsToRemove;
RedirectsToRemove.Reserve(NumInactive);
Idx = 0;
for (const TPair<FSoftObjectPath, FStreamable*>& Pair : StreamableItems)
{
if (InactiveStreamables[Idx++])
{
RedirectsToRemove.Add(Pair.Key);
}
}
for (TStreamableRedirects::TIterator It(StreamableRedirects); It; ++It)
{
if (RedirectsToRemove.Contains(It.Value().NewPath))
{
It.RemoveCurrent();
}
}
}
// Delete and remove inactive streamables
Idx = 0;
for (TStreamableMap::TIterator It(StreamableItems); It; ++It)
{
if (InactiveStreamables[Idx++])
{
It->Value->FreeHandles();
delete It->Value;
It.RemoveCurrent();
}
}
}
void FStreamableManager::AddReferencedObjects(FReferenceCollector& Collector)
{
// If there are active streamable handles in the editor, this will cause the user to Force Delete, which is irritating but necessary because weak pointers cannot be used here
for (TPair<FSoftObjectPath, FStreamable*>& Pair : StreamableItems)
{
Collector.AddStableReference(&Pair.Value->Target);
}
for (TPair<FSoftObjectPath, FRedirectedPath>& Pair : StreamableRedirects)
{
Collector.AddStableReference(&Pair.Value.LoadedRedirector);
}
}
const FString& FStreamableManager::GetManagerName() const
{
return ManagerName;
}
void FStreamableManager::SetManagerName(FString InName)
{
ManagerName = MoveTemp(InName);
}
bool FStreamableManager::GetReferencerPropertyName(UObject* Object, FString& OutPropertyName) const
{
if (Object)
{
// Report the first handle
for (TStreamableMap::TConstIterator It(StreamableItems); It; ++It)
{
const FStreamable* Existing = It.Value();
check(Existing);
if (Existing->Target == Object)
{
if (Existing->ActiveHandles.Num() > 0)
{
OutPropertyName = Existing->ActiveHandles[0]->GetDebugName();
return true;
}
if (Existing->LoadingHandles.Num() > 0)
{
OutPropertyName = Existing->LoadingHandles[0]->GetDebugName();
return true;
}
}
}
}
return false;
}
FStreamable* FStreamableManager::FindStreamable(const FSoftObjectPath& Target) const
{
FStreamable* Existing = StreamableItems.FindRef(Target);
if (!Existing)
{
return StreamableItems.FindRef(ResolveRedirects(Target));
}
return Existing;
}
FStreamable* FStreamableManager::StreamInternal(const FSoftObjectPath& InTargetName, TAsyncLoadPriority Priority, TSharedRef<FStreamableHandle> Handle)
{
check(IsInGameThread());
UE_LOG(LogStreamableManager, Verbose, TEXT("Asynchronous load %s"), *InTargetName.ToString());
FSoftObjectPath TargetName = ResolveRedirects(InTargetName);
FStreamable* Existing = StreamableItems.FindRef(TargetName);
if (Existing)
{
if (Existing->bAsyncLoadRequestOutstanding)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Already in progress %s"), *TargetName.ToString());
check(!Existing->Target); // should not be a load request unless the target is invalid
ensure(IsAsyncLoading()); // Nothing should be pending if there is no async loading happening
// Don't return as we potentially want to sync load it
}
if (Existing->Target)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Already Loaded %s"), *TargetName.ToString());
return Existing;
}
}
else
{
Existing = StreamableItems.Add(TargetName, new FStreamable());
}
if (!Existing->bAsyncLoadRequestOutstanding)
{
FindInMemory(TargetName, Existing);
}
if (!Existing->Target)
{
// Disable failed flag as it may have been added at a later point
Existing->bLoadFailed = false;
FUObjectThreadContext& ThreadContext = FUObjectThreadContext::Get();
// If async loading isn't safe or it's forced on, we have to do a sync load which will flush all async loading
if (GIsInitialLoad || ThreadContext.IsInConstructor > 0 || bForceSynchronousLoads)
{
FRedirectedPath RedirectedPath;
UE_LOG(LogStreamableManager, Verbose, TEXT(" Static loading %s"), *TargetName.ToString());
Existing->Target = StaticLoadObject(UObject::StaticClass(), nullptr, *TargetName.ToString());
// Need to manually detect redirectors because the above call only expects to load a UObject::StaticClass() type
UObjectRedirector* Redir = Cast<UObjectRedirector>(Existing->Target);
if (Redir)
{
TargetName = HandleLoadedRedirector(Redir, TargetName, Existing);
}
if (Existing->Target)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Static loaded %s"), *Existing->Target->GetFullName());
}
else
{
Existing->bLoadFailed = true;
UE_LOG(LogStreamableManager, Log, TEXT("Failed attempt to load %s"), *TargetName.ToString());
}
Existing->bAsyncLoadRequestOutstanding = false;
}
else
{
// We always queue a new request in case the existing one gets cancelled
FString Package = TargetName.ToString();
int32 FirstDot = Package.Find(TEXT("."), ESearchCase::CaseSensitive);
if (FirstDot != INDEX_NONE)
{
Package.LeftInline(FirstDot,false);
}
FPackagePath PackagePath;
if (!FPackagePath::TryFromPackageName(Package, PackagePath))
{
UE_LOG(LogStreamableManager, Error, TEXT("Failed attempt to load %s; it is not a valid LongPackageName"), *Package);
Existing->bLoadFailed = true;
Existing->bAsyncLoadRequestOutstanding = false;
}
else
{
Existing->bLoadFailed = false;
Existing->bAsyncLoadRequestOutstanding = true;
LoadPackageAsync(PackagePath,
NAME_None /* PackageNameToCreate */,
FLoadPackageAsyncDelegate::CreateSP(Handle, &FStreamableHandle::AsyncLoadCallbackWrapper, TargetName),
PKG_None /* InPackageFlags */,
INDEX_NONE /* InPIEInstanceID */,
Priority /* InPackagePriority */);
}
}
}
return Existing;
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestAsyncLoad(TArray<FSoftObjectPath> TargetsToStream, FStreamableDelegate DelegateToCall, TAsyncLoadPriority Priority, bool bManageActiveHandle, bool bStartStalled, FString DebugName)
{
LLM_SCOPE(ELLMTag::StreamingManager);
// Schedule a new callback, this will get called when all related async loads are completed
TSharedRef<FStreamableHandle> NewRequest = MakeShareable(new FStreamableHandle());
NewRequest->CompleteDelegate = DelegateToCall;
NewRequest->OwningManager = this;
NewRequest->RequestedAssets = MoveTemp(TargetsToStream);
#if (!PLATFORM_IOS && !PLATFORM_ANDROID)
NewRequest->DebugName = MoveTemp(DebugName);
#endif
NewRequest->Priority = Priority;
int32 NumValidRequests = NewRequest->RequestedAssets.Num();
TSet<FSoftObjectPath> TargetSet;
TargetSet.Reserve(NumValidRequests);
for (const FSoftObjectPath& TargetName : NewRequest->RequestedAssets)
{
if (TargetName.IsNull())
{
--NumValidRequests;
continue;
}
else if (FPackageName::IsShortPackageName(TargetName.GetLongPackageFName()))
{
UE_LOG(LogStreamableManager, Error, TEXT("RequestAsyncLoad(%s) called with invalid package name %s"), *DebugName, *TargetName.ToString());
NewRequest->CancelHandle();
return nullptr;
}
TargetSet.Add(TargetName);
}
if (NumValidRequests == 0)
{
// Original array was empty or all null
UE_LOG(LogStreamableManager, Display, TEXT("RequestAsyncLoad(%s) called with empty or only null assets!"), *DebugName);
NewRequest->CancelHandle();
return nullptr;
}
else if (NewRequest->RequestedAssets.Num() != NumValidRequests)
{
FString RequestedSet;
for (auto It = NewRequest->RequestedAssets.CreateIterator(); It; ++It)
{
FSoftObjectPath& Asset = *It;
if (!RequestedSet.IsEmpty())
{
RequestedSet += TEXT(", ");
}
RequestedSet += Asset.ToString();
// Remove null entries
if (Asset.IsNull())
{
It.RemoveCurrent();
}
}
// Some valid, some null
UE_LOG(LogStreamableManager, Display, TEXT("RequestAsyncLoad(%s) called with both valid and null assets, null assets removed from %s!"), *DebugName, *RequestedSet);
}
if (TargetSet.Num() != NewRequest->RequestedAssets.Num())
{
#if UE_BUILD_DEBUG
FString RequestedSet;
for (const FSoftObjectPath& Asset : NewRequest->RequestedAssets)
{
if (!RequestedSet.IsEmpty())
{
RequestedSet += TEXT(", ");
}
RequestedSet += Asset.ToString();
}
UE_LOG(LogStreamableManager, Verbose, TEXT("RequestAsyncLoad(%s) called with duplicate assets, duplicates removed from %s!"), *DebugName, *RequestedSet);
#endif
NewRequest->RequestedAssets = TargetSet.Array();
}
if (bManageActiveHandle)
{
// This keeps a reference around until explicitly released
ManagedActiveHandles.Add(NewRequest);
}
if (bStartStalled)
{
NewRequest->bStalled = true;
}
else
{
StartHandleRequests(NewRequest);
}
return NewRequest;
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestAsyncLoad(const FSoftObjectPath& TargetToStream, FStreamableDelegate DelegateToCall, TAsyncLoadPriority Priority, bool bManageActiveHandle, bool bStartStalled, FString DebugName)
{
return RequestAsyncLoad(TArray<FSoftObjectPath>{TargetToStream}, MoveTemp(DelegateToCall), Priority, bManageActiveHandle, bStartStalled, MoveTemp(DebugName));
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestAsyncLoad(TArray<FSoftObjectPath> TargetsToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority, bool bManageActiveHandle, bool bStartStalled, FString DebugName)
{
return RequestAsyncLoad(MoveTemp(TargetsToStream), FStreamableDelegate::CreateLambda( MoveTemp( Callback ) ), Priority, bManageActiveHandle, bStartStalled, MoveTemp(DebugName));
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestAsyncLoad(const FSoftObjectPath& TargetToStream, TFunction<void()>&& Callback, TAsyncLoadPriority Priority, bool bManageActiveHandle, bool bStartStalled, FString DebugName)
{
return RequestAsyncLoad(TargetToStream, FStreamableDelegate::CreateLambda( MoveTemp( Callback ) ), Priority, bManageActiveHandle, bStartStalled, MoveTemp(DebugName));
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestSyncLoad(TArray<FSoftObjectPath> TargetsToStream, bool bManageActiveHandle, FString DebugName)
{
// If in async loading thread or from callback always do sync as recursive tick is unsafe
// If in EDL always do sync as EDL internally avoids flushing
// Otherwise, only do a sync load if there are no background sync loads, this is faster but will cause a sync flush
bForceSynchronousLoads = IsInAsyncLoadingThread() || IsEventDrivenLoaderEnabled() || !IsAsyncLoading();
// Do an async load and wait to complete. In some cases this will do a sync load due to safety issues
TSharedPtr<FStreamableHandle> Request = RequestAsyncLoad(MoveTemp(TargetsToStream), FStreamableDelegate(), AsyncLoadHighPriority, bManageActiveHandle, false, MoveTemp(DebugName));
bForceSynchronousLoads = false;
if (Request.IsValid())
{
EAsyncPackageState::Type Result = Request->WaitUntilComplete();
ensureMsgf(Result == EAsyncPackageState::Complete, TEXT("RequestSyncLoad of %s resulted in bad async load result %d"), *Request->DebugName, Result);
ensureMsgf(Request->HasLoadCompleted(), TEXT("RequestSyncLoad of %s completed early, not actually completed!"), *Request->DebugName);
}
return Request;
}
TSharedPtr<FStreamableHandle> FStreamableManager::RequestSyncLoad(const FSoftObjectPath& TargetToStream, bool bManageActiveHandle, FString DebugName)
{
return RequestSyncLoad(TArray<FSoftObjectPath>{TargetToStream}, bManageActiveHandle, MoveTemp(DebugName));
}
void FStreamableManager::StartHandleRequests(TSharedRef<FStreamableHandle> Handle)
{
TRACE_LOADTIME_REQUEST_GROUP_SCOPE(TEXT("StreamableManager - %s"), *Handle->GetDebugName());
TArray<FStreamable *> ExistingStreamables;
ExistingStreamables.Reserve(Handle->RequestedAssets.Num());
for (int32 i = 0; i < Handle->RequestedAssets.Num(); i++)
{
FStreamable* Existing = StreamInternal(Handle->RequestedAssets[i], Handle->Priority, Handle);
check(Existing);
ExistingStreamables.Add(Existing);
Existing->AddLoadingRequest(Handle);
}
// Go through and complete loading anything that's already in memory, this may call the callback right away
for (int32 i = 0; i < Handle->RequestedAssets.Num(); i++)
{
FStreamable* Existing = ExistingStreamables[i];
if (Existing && (Existing->Target || Existing->bLoadFailed))
{
Existing->bAsyncLoadRequestOutstanding = false;
CheckCompletedRequests(Handle->RequestedAssets[i], Existing);
}
}
}
UObject* FStreamableManager::LoadSynchronous(const FSoftObjectPath& Target, bool bManageActiveHandle, TSharedPtr<FStreamableHandle>* RequestHandlePointer)
{
TSharedPtr<FStreamableHandle> Request = RequestSyncLoad(Target, bManageActiveHandle, FString::Printf(TEXT("LoadSynchronous of %s"), *Target.ToString()));
if (RequestHandlePointer)
{
(*RequestHandlePointer) = Request;
}
if (Request.IsValid())
{
UObject* Result = Request->GetLoadedAsset();
if (!Result)
{
UE_LOG(LogStreamableManager, Verbose, TEXT("LoadSynchronous failed for load of %s! File is missing or there is a loading system problem"), *Target.ToString());
}
return Result;
}
return nullptr;
}
void FStreamableManager::FindInMemory(FSoftObjectPath& InOutTargetName, struct FStreamable* Existing, UPackage* Package)
{
check(Existing);
check(!Existing->bAsyncLoadRequestOutstanding);
UE_LOG(LogStreamableManager, Verbose, TEXT(" Searching in memory for %s"), *InOutTargetName.ToString());
Existing->Target = nullptr;
UObject* Asset = Package ?
StaticFindObjectFast(UObject::StaticClass(), Package, InOutTargetName.GetAssetFName()) :
StaticFindObject(UObject::StaticClass(), InOutTargetName.GetAssetPath(), /*ExactClass*/false);
if (Asset)
{
if (InOutTargetName.IsAsset())
{
Existing->Target = Asset;
}
else if (InOutTargetName.IsSubobject())
{
Existing->Target = StaticFindObject(UObject::StaticClass(), Asset, *InOutTargetName.GetSubPathString());
}
}
checkSlow(Existing->Target == StaticFindObject(UObject::StaticClass(), nullptr, *InOutTargetName.ToString()));
if (Existing->Target && Existing->Target->HasAnyInternalFlags(EInternalObjectFlags::AsyncLoading))
{
// This can get called from PostLoad on async loaded objects, if it is we do not want to return partially loaded objects and instead want to register for their full load
Existing->Target = nullptr;
}
UObjectRedirector* Redir = Cast<UObjectRedirector>(Existing->Target);
if (Redir)
{
InOutTargetName = HandleLoadedRedirector(Redir, InOutTargetName, Existing);
}
if (Existing->Target)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Found in memory %s"), *Existing->Target->GetFullName());
Existing->bLoadFailed = false;
}
}
void FStreamableManager::AsyncLoadCallback(FSoftObjectPath TargetName, UPackage* Package)
{
check(IsInGameThread());
FStreamable* Existing = FindStreamable(TargetName);
UE_LOG(LogStreamableManager, Verbose, TEXT("Stream Complete callback %s"), *TargetName.ToString());
if (Existing)
{
if (Existing->bAsyncLoadRequestOutstanding)
{
Existing->bAsyncLoadRequestOutstanding = false;
if (!Existing->Target)
{
FindInMemory(TargetName, Existing, Package);
}
CheckCompletedRequests(TargetName, Existing);
}
else
{
UE_LOG(LogStreamableManager, Verbose, TEXT("AsyncLoadCallback called for %s when not waiting on a load request, was loaded early by sync load"), *TargetName.ToString());
}
if (Existing->Target)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Found target %s"), *Existing->Target->GetFullName());
}
else
{
// Async load failed to find the object
Existing->bLoadFailed = true;
UE_LOG(LogStreamableManager, Verbose, TEXT(" Failed async load for %s"), *TargetName.ToString());
}
}
else
{
UE_LOG(LogStreamableManager, Error, TEXT("Can't find streamable for %s in AsyncLoadCallback!"), *TargetName.ToString());
}
}
void FStreamableManager::CheckCompletedRequests(const FSoftObjectPath& Target, struct FStreamable* Existing)
{
static int32 RecursiveCount = 0;
ensure(RecursiveCount == 0);
RecursiveCount++;
// Release these handles at end
TArray<TSharedRef<FStreamableHandle>> HandlesToComplete;
TArray<TSharedRef<FStreamableHandle>> HandlesToRelease;
for (TSharedRef<FStreamableHandle>& Handle : Existing->LoadingHandles)
{
ensure(Handle->WasCanceled() || Handle->OwningManager == this);
// Decrement related requests, and call delegate if all are done and request is still active
Handle->StreamablesLoading--;
if (Handle->StreamablesLoading == 0)
{
if (Handle->bReleaseWhenLoaded)
{
HandlesToRelease.Add(Handle);
}
HandlesToComplete.Add(Handle);
}
}
Existing->LoadingHandles.Empty();
for (TSharedRef<FStreamableHandle>& Handle : HandlesToComplete)
{
Handle->CompleteLoad();
}
for (TSharedRef<FStreamableHandle>& Handle : HandlesToRelease)
{
Handle->ReleaseHandle();
}
// HandlesToRelease might get deleted when function ends
RecursiveCount--;
}
void FStreamableManager::RemoveReferencedAsset(const FSoftObjectPath& Target, TSharedRef<FStreamableHandle> Handle)
{
if (Target.IsNull())
{
return;
}
ensureMsgf(Handle->OwningManager == this, TEXT("RemoveReferencedAsset called on wrong streamable manager for target %s"), *Target.ToString());
FStreamable* Existing = FindStreamable(Target);
// This should always be in the active handles list
if (ensureMsgf(Existing, TEXT("Failed to find existing streamable for %s"), *Target.ToString()))
{
ensureMsgf(Existing->ActiveHandles.RemoveSwap(&Handle.Get(), /* shrink */ false) > 0, TEXT("Failed to remove active handle for %s"), *Target.ToString());
// Try removing from loading list if it's still there, this won't call the callback as it's being called from cancel
// This may remove more than one copy if streamables were merged
int32 LoadingRemoved = Existing->LoadingHandles.RemoveSwap(Handle);
if (LoadingRemoved > 0)
{
Handle->StreamablesLoading -= LoadingRemoved;
if (Existing->LoadingHandles.Num() == 0)
{
// All requests cancelled, remove loading flag
Existing->bAsyncLoadRequestOutstanding = false;
}
}
}
}
bool FStreamableManager::AreAllAsyncLoadsComplete() const
{
check(IsInGameThread());
for (const TPair<FSoftObjectPath, FStreamable*>& Item : StreamableItems)
{
const FStreamable* Streamable = Item.Value;
if (Streamable && Streamable->bAsyncLoadRequestOutstanding)
{
return false;
}
}
return true;
}
bool FStreamableManager::IsAsyncLoadComplete(const FSoftObjectPath& Target) const
{
// Failed loads count as success
check(IsInGameThread());
FStreamable* Existing = FindStreamable(Target);
return !Existing || !Existing->bAsyncLoadRequestOutstanding;
}
UObject* FStreamableManager::GetStreamed(const FSoftObjectPath& Target) const
{
check(IsInGameThread());
FStreamable* Existing = FindStreamable(Target);
if (Existing && Existing->Target)
{
return Existing->Target;
}
return nullptr;
}
void FStreamableManager::Unload(const FSoftObjectPath& Target)
{
check(IsInGameThread());
TArray<TSharedRef<FStreamableHandle>> HandleList;
if (GetActiveHandles(Target, HandleList, true))
{
for (TSharedRef<FStreamableHandle> Handle : HandleList)
{
Handle->ReleaseHandle();
}
}
else
{
UE_LOG(LogStreamableManager, Verbose, TEXT("Attempt to unload %s, but it isn't loaded"), *Target.ToString());
}
}
TSharedPtr<FStreamableHandle> FStreamableManager::CreateCombinedHandle(const TConstArrayView<TSharedPtr<FStreamableHandle> >& ChildHandles, const FString& DebugName, EStreamableManagerCombinedHandleOptions Options)
{
if (!ensure(ChildHandles.Num() > 0))
{
return nullptr;
}
TSharedRef<FStreamableHandle> NewRequest = MakeShareable(new FStreamableHandle());
NewRequest->OwningManager = this;
NewRequest->bIsCombinedHandle = true;
NewRequest->DebugName = DebugName;
static const auto RemapHandleParentRelationship = [](TSharedPtr<FStreamableHandle>& IndividualHandle, TSharedRef<FStreamableHandle>& MergedHandle)
{
// break backlinking, inject new forelinking (such that our old parent handles now reference the new MergedHandle - our old parent's ChildHandles array will contain MergedHandle, and MergedHandle is our new Parent)...
for (int32 Index = 0, Num = IndividualHandle->ParentHandles.Num(); Index < Num; ++Index)
{
TSharedPtr<FStreamableHandle> Parent = IndividualHandle->ParentHandles[Index].Pin();
// even though we should run this check (via invoking this lambda) prior to adding MergedHandle to our ParentHandles, it is still possible if the array is not unique
if (Parent == MergedHandle)
{
continue;
}
if (Parent.IsValid())
{
const int32 FoundIndex = Parent->ChildHandles.Find(IndividualHandle);
if (ensure(Parent->ChildHandles.IsValidIndex(FoundIndex)))
{
// stomp the old link with the new merged handle
Parent->ChildHandles[FoundIndex] = MergedHandle;
}
else
{
// should be impossible, but at least we can add the merged handle for tracking purposes...
Parent->ChildHandles.Add(MergedHandle);
}
MergedHandle->ParentHandles.Add(Parent);
}
// no longer need the parent handle reference in this handle's parents. The merged handle is within our parent handles, and has us in its ChildHandles.
IndividualHandle->ParentHandles.RemoveAtSwap(Index);
Index--;
Num--;
}
};
TStringBuilder<256> DebugNameBuilder;
if (EnumHasAllFlags(Options, EStreamableManagerCombinedHandleOptions::MergeDebugNames))
{
DebugNameBuilder.Appendf(TEXT("%s: Merged("), *DebugName);
}
bool bFormattingFirstPass = true;
for (TSharedPtr<FStreamableHandle> ChildHandle : ChildHandles)
{
if (!ChildHandle.IsValid())
{
if (EnumHasAllFlags(Options, EStreamableManagerCombinedHandleOptions::SkipNulls))
{
continue;
}
else
{
ensureMsgf(ChildHandle.IsValid(), TEXT("A child handle is not valid and SkipNulls flag was not used, returning nullptr"));
return nullptr;
}
}
ensure(ChildHandle->OwningManager == this);
if (EnumHasAllFlags(Options, EStreamableManagerCombinedHandleOptions::MergeDebugNames))
{
if (bFormattingFirstPass)
{
DebugNameBuilder.Appendf(TEXT("%s"), *ChildHandle->DebugName);
bFormattingFirstPass = false;
}
else
{
DebugNameBuilder.Appendf(TEXT(", %s"), *ChildHandle->DebugName);
}
}
if (EnumHasAllFlags(Options, EStreamableManagerCombinedHandleOptions::RedirectParents))
{
RemapHandleParentRelationship(ChildHandle, NewRequest);
}
ChildHandle->ParentHandles.Add(NewRequest);
NewRequest->ChildHandles.Add(ChildHandle);
if (ChildHandle->bLoadCompleted)
{
++NewRequest->CompletedChildCount;
}
if (ChildHandle->bCanceled)
{
++NewRequest->CanceledChildCount;
}
}
if (EnumHasAllFlags(Options, EStreamableManagerCombinedHandleOptions::MergeDebugNames))
{
DebugNameBuilder += TEXT(")");
NewRequest->DebugName = *DebugNameBuilder;
}
// Add to pending list so these handles don't free when not referenced
PendingCombinedHandles.Add(NewRequest);
// This may already be complete
NewRequest->UpdateCombinedHandle();
return NewRequest;
}
bool FStreamableManager::GetActiveHandles(const FSoftObjectPath& Target, TArray<TSharedRef<FStreamableHandle>>& HandleList, bool bOnlyManagedHandles) const
{
check(IsInGameThread());
FStreamable* Existing = FindStreamable(Target);
if (Existing && Existing->ActiveHandles.Num() > 0)
{
for (FStreamableHandle* ActiveHandle : Existing->ActiveHandles)
{
ensure(ActiveHandle->OwningManager == this);
TSharedRef<FStreamableHandle> HandleRef = ActiveHandle->AsShared();
if (!bOnlyManagedHandles || ManagedActiveHandles.Contains(HandleRef))
{
// Only add each handle once, we can have duplicates in the source list
HandleList.AddUnique(MoveTemp(HandleRef));
}
}
return HandleList.Num() > 0;
}
return false;
}
FSoftObjectPath FStreamableManager::ResolveRedirects(const FSoftObjectPath& Target) const
{
FRedirectedPath const* Redir = StreamableRedirects.Find(Target);
if (Redir)
{
check(Target != Redir->NewPath);
UE_LOG(LogStreamableManager, Verbose, TEXT("Redirected %s -> %s"), *Target.ToString(), *Redir->NewPath.ToString());
return Redir->NewPath;
}
return Target;
}
FSoftObjectPath FStreamableManager::HandleLoadedRedirector(UObjectRedirector* LoadedRedirector, FSoftObjectPath RequestedPath, FStreamable* RequestedStreamable)
{
UE_LOG(LogStreamableManager, Verbose, TEXT(" Found redirect %s"), *LoadedRedirector->GetPathName());
// Need to follow the redirector chain
while (UObjectRedirector* Redirector = Cast<UObjectRedirector>(RequestedStreamable->Target))
{
RequestedStreamable->Target = Redirector->DestinationObject;
if (!RequestedStreamable->Target)
{
RequestedStreamable->bLoadFailed = true;
UE_LOG(LogStreamableManager, Warning, TEXT("Destination of redirector was not found %s -> %s."), *RequestedPath.ToString(), *Redirector->GetPathName());
return RequestedPath;
}
}
FSoftObjectPath NewPath(RequestedStreamable->Target->GetPathName());
UE_LOG(LogStreamableManager, Verbose, TEXT(" Which redirected to %s"), *NewPath.ToString());
// We add the originally loaded redirector to GC keep alive, this will keep entire chain alive
StreamableRedirects.Add(RequestedPath, FRedirectedPath(NewPath, LoadedRedirector));
// Remove the requested streamable from it's old location
StreamableItems.Remove(RequestedPath);
if (FStreamable** FoundStreamable = StreamableItems.Find(NewPath))
{
// We found an existing streamable, need to merge the handles and delete old streamable
// This may result in the same handle being in the list twice! But the rest of the code is ready for that
// We let LoadFailed and InProgress stay as false on the new streamable because we've successfully loaded an object
RequestedStreamable->LoadingHandles.Append(MoveTemp((*FoundStreamable)->LoadingHandles));
RequestedStreamable->ActiveHandles.Append(MoveTemp((*FoundStreamable)->ActiveHandles));
// No handles remain so need to FreeHandles()
delete *FoundStreamable;
}
// Add requested streamable with new path
StreamableItems.Add(NewPath, RequestedStreamable);
return NewPath;
}