RealtimeStyleTransferRuntime/Plugins/AsyncMixin/Source/Private/AsyncMixin.cpp

596 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AsyncMixin.h"
#include "Containers/Ticker.h"
#include "Containers/UnrealString.h"
#include "CoreGlobals.h"
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
#include "Logging/LogCategory.h"
#include "Logging/LogMacros.h"
#include "Misc/AssertionMacros.h"
#include "Stats/Stats.h"
#include "Stats/Stats2.h"
#include "Trace/Detail/Channel.h"
#include "UObject/NameTypes.h"
#include "UObject/PrimaryAssetId.h"
DEFINE_LOG_CATEGORY_STATIC(LogAsyncMixin, Log, All);
TMap<FAsyncMixin*, TSharedRef<FAsyncMixin::FLoadingState>> FAsyncMixin::Loading;
FAsyncMixin::FAsyncMixin()
{
}
FAsyncMixin::~FAsyncMixin()
{
check(IsInGameThread());
// Removing the loading state will cancel any pending loadings it was
// monitoring, and shouldn't receive any future callbacks for completion.
Loading.Remove(this);
}
const FAsyncMixin::FLoadingState& FAsyncMixin::GetLoadingStateConst() const
{
check(IsInGameThread());
return Loading.FindChecked(this).Get();
}
FAsyncMixin::FLoadingState& FAsyncMixin::GetLoadingState()
{
check(IsInGameThread());
if (TSharedRef<FLoadingState>* LoadingState = Loading.Find(this))
{
return (*LoadingState).Get();
}
return Loading.Add(this, MakeShared<FLoadingState>(*this)).Get();
}
bool FAsyncMixin::HasLoadingState() const
{
check(IsInGameThread());
return Loading.Contains(this);
}
void FAsyncMixin::CancelAsyncLoading()
{
// Don't create the loading state if we don't have anything pending.
if (HasLoadingState())
{
GetLoadingState().CancelAndDestroy();
}
}
bool FAsyncMixin::IsAsyncLoadingInProgress() const
{
// Don't create the loading state if we don't have anything pending.
if (HasLoadingState())
{
return GetLoadingStateConst().IsLoadingInProgress();
}
return false;
}
bool FAsyncMixin::IsLoadingInProgressOrPending() const
{
if (HasLoadingState())
{
return GetLoadingStateConst().IsLoadingInProgressOrPending();
}
return false;
}
void FAsyncMixin::AsyncLoad(FSoftObjectPath SoftObjectPath, const FSimpleDelegate& DelegateToCall)
{
GetLoadingState().AsyncLoad(SoftObjectPath, DelegateToCall);
}
void FAsyncMixin::AsyncLoad(const TArray<FSoftObjectPath>& SoftObjectPaths, const FSimpleDelegate& DelegateToCall)
{
GetLoadingState().AsyncLoad(SoftObjectPaths, DelegateToCall);
}
void FAsyncMixin::AsyncPreloadPrimaryAssetsAndBundles(const TArray<FPrimaryAssetId>& AssetIds, const TArray<FName>& LoadBundles, const FSimpleDelegate& DelegateToCall)
{
GetLoadingState().AsyncPreloadPrimaryAssetsAndBundles(AssetIds, LoadBundles, DelegateToCall);
}
void FAsyncMixin::AsyncCondition(TSharedRef<FAsyncCondition> Condition, const FSimpleDelegate& Callback)
{
GetLoadingState().AsyncCondition(Condition, Callback);
}
void FAsyncMixin::AsyncEvent(const FSimpleDelegate& Callback)
{
GetLoadingState().AsyncEvent(Callback);
}
void FAsyncMixin::StartAsyncLoading()
{
// If we don't actually have any loading state because they've not queued anything to load,
// just immediately start and finish the operation by calling the callbacks, no point in allocating
// the memory just to de-allocate it.
if (IsLoadingInProgressOrPending())
{
GetLoadingState().Start();
}
else
{
OnStartedLoading();
OnFinishedLoading();
}
}
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
FAsyncMixin::FLoadingState::FLoadingState(FAsyncMixin& InOwner)
: OwnerRef(InOwner)
{
}
FAsyncMixin::FLoadingState::~FLoadingState()
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FAsyncMixin_FLoadingState_DestroyThisMemoryDelegate);
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Destroy LoadingState (Done)"), this);
// If we get destroyed, need to cancel whatever we're doing and cancel any
// pending destruction - as we're already on the way out.
CancelOnly(/*bDestroying*/true);
CancelDestroyThisMemory(/*bDestroying*/true);
}
void FAsyncMixin::FLoadingState::CancelOnly(bool bDestroying)
{
if (!bDestroying)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Cancel"), this);
}
CancelStartTimer();
for (TUniquePtr<FAsyncStep>& Step : AsyncSteps)
{
Step->Cancel();
}
// Moving the memory to another array so we don't crash.
// There was an issue where the Step would get corrupted because we were calling Reset() on the array.
AsyncStepsPendingDestruction = MoveTemp(AsyncSteps);
bPreloadedBundles = false;
bHasStarted = false;
CurrentAsyncStep = 0;
}
void FAsyncMixin::FLoadingState::CancelAndDestroy()
{
CancelOnly(/*bDestroying*/false);
RequestDestroyThisMemory();
}
void FAsyncMixin::FLoadingState::CancelDestroyThisMemory(bool bDestroying)
{
// If we've schedule the memory to be deleted we need to abort that.
if (IsPendingDestroy())
{
if (!bDestroying)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Destroy LoadingState (Canceled)"), this);
}
FTSTicker::GetCoreTicker().RemoveTicker(DestroyMemoryDelegate);
DestroyMemoryDelegate.Reset();
}
}
void FAsyncMixin::FLoadingState::RequestDestroyThisMemory()
{
// If we're already pending to destroy this memory, just ignore.
if (!IsPendingDestroy())
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Destroy LoadingState (Requested)"), this);
DestroyMemoryDelegate = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float DeltaTime) {
// Remove any memory we were using.
FAsyncMixin::Loading.Remove(&OwnerRef);
return false;
}));
}
}
void FAsyncMixin::FLoadingState::CancelStartTimer()
{
if (StartTimerDelegate.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(StartTimerDelegate);
StartTimerDelegate.Reset();
}
}
void FAsyncMixin::FLoadingState::Start()
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Start (Current Progress %d/%d)"), this, CurrentAsyncStep + 1, AsyncSteps.Num());
// Cancel any pending kickoff load requests.
CancelStartTimer();
bool bStartingStepFound = false;
if (!bHasStarted)
{
bHasStarted = true;
OwnerRef.OnStartedLoading();
}
TryCompleteAsyncLoading();
}
void FAsyncMixin::FLoadingState::AsyncLoad(FSoftObjectPath SoftObjectPath, const FSimpleDelegate& DelegateToCall)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncLoad '%s'"), this, *SoftObjectPath.ToString());
AsyncSteps.Add(
MakeUnique<FAsyncStep>(
DelegateToCall,
UAssetManager::GetStreamableManager().RequestAsyncLoad(SoftObjectPath, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, false, false, TEXT("AsyncMixin"))
)
);
TryScheduleStart();
}
void FAsyncMixin::FLoadingState::AsyncLoad(const TArray<FSoftObjectPath>& SoftObjectPaths, const FSimpleDelegate& DelegateToCall)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncLoad [%s]"), this, *FString::JoinBy(SoftObjectPaths, TEXT(", "), [](const FSoftObjectPath& SoftObjectPath) { return FString::Printf(TEXT("'%s'"), *SoftObjectPath.ToString()); }));
AsyncSteps.Add(
MakeUnique<FAsyncStep>(
DelegateToCall,
UAssetManager::GetStreamableManager().RequestAsyncLoad(SoftObjectPaths, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, false, false, TEXT("AsyncMixin"))
)
);
TryScheduleStart();
}
void FAsyncMixin::FLoadingState::AsyncPreloadPrimaryAssetsAndBundles(const TArray<FPrimaryAssetId>& AssetIds, const TArray<FName>& LoadBundles, const FSimpleDelegate& DelegateToCall)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncPreload Assets [%s], Bundles[%s]"),
this,
*FString::JoinBy(AssetIds, TEXT(", "), [](const FPrimaryAssetId& AssetId) { return *AssetId.ToString(); }),
*FString::JoinBy(LoadBundles, TEXT(", "), [](const FName& LoadBundle) { return *LoadBundle.ToString(); })
);
TSharedPtr<FStreamableHandle> StreamingHandle;
if (AssetIds.Num() > 0)
{
bPreloadedBundles = true;
const bool bLoadRecursive = true;
StreamingHandle = UAssetManager::Get().PreloadPrimaryAssets(AssetIds, LoadBundles, bLoadRecursive);
}
AsyncSteps.Add(MakeUnique<FAsyncStep>(DelegateToCall, StreamingHandle));
TryScheduleStart();
}
void FAsyncMixin::FLoadingState::AsyncCondition(TSharedRef<FAsyncCondition> Condition, const FSimpleDelegate& DelegateToCall)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncCondition '0x%X'"), this, &Condition.Get());
AsyncSteps.Add(MakeUnique<FAsyncStep>(DelegateToCall, Condition));
TryScheduleStart();
}
void FAsyncMixin::FLoadingState::AsyncEvent(const FSimpleDelegate& DelegateToCall)
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncEvent"), this);
AsyncSteps.Add(MakeUnique<FAsyncStep>(DelegateToCall));
TryScheduleStart();
}
void FAsyncMixin::FLoadingState::TryScheduleStart()
{
CancelDestroyThisMemory(/*bDestroying*/false);
// In the event the user forgets to start async loading, we'll begin doing it next frame.
if (!StartTimerDelegate.IsValid())
{
StartTimerDelegate = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float DeltaTime) {
QUICK_SCOPE_CYCLE_COUNTER(STAT_FAsyncMixin_FLoadingState_TryScheduleStartDelegate);
Start();
return false;
}));
}
}
bool FAsyncMixin::FLoadingState::IsLoadingInProgress() const
{
if (AsyncSteps.Num() > 0)
{
if (CurrentAsyncStep < AsyncSteps.Num())
{
if (CurrentAsyncStep == (AsyncSteps.Num() - 1))
{
return AsyncSteps[CurrentAsyncStep]->IsLoadingInProgress();
}
// If we know it's a valid index, but not the last one, then we know we're still loading,
// if it's not a valid index, we know there's no loading, or we're beyond any loading.
return true;
}
}
return false;
}
bool FAsyncMixin::FLoadingState::IsLoadingInProgressOrPending() const
{
return StartTimerDelegate.IsValid() || IsLoadingInProgress();
}
bool FAsyncMixin::FLoadingState::IsPendingDestroy() const
{
return DestroyMemoryDelegate.IsValid();
}
void FAsyncMixin::FLoadingState::TryCompleteAsyncLoading()
{
// If we haven't started when we get this callback it means we've already completed
// and this is some other callback finishing on the same frame/stack that we need to avoid
// doing anything with until the memory is finished being deleted.
if (!bHasStarted)
{
return;
}
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] TryCompleteAsyncLoading - (Current Progress %d/%d)"), this, CurrentAsyncStep + 1, AsyncSteps.Num());
while (CurrentAsyncStep < AsyncSteps.Num())
{
FAsyncStep* Step = AsyncSteps[CurrentAsyncStep].Get();
if (Step->IsLoadingInProgress())
{
if (!Step->IsCompleteDelegateBound())
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Step %d - Still Loading (Listening)"), this, CurrentAsyncStep + 1);
const bool bBound = Step->BindCompleteDelegate(FSimpleDelegate::CreateSP(this, &FLoadingState::TryCompleteAsyncLoading));
ensureMsgf(bBound, TEXT("This is not intended to return false. We're checking if it's loaded above, this should definitely return true."));
}
else
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Step %d - Still Loading (Waiting)"), this, CurrentAsyncStep + 1);
}
break;
}
else
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] Step %d - Completed (Calling User)"), this, CurrentAsyncStep + 1);
// Always advance the CurrentAsyncStep, before calling the user callback, it's possible they might
// add new work, and try and start again, so we need to be ready for the next bit.
CurrentAsyncStep++;
Step->ExecuteUserCallback();
}
}
// If we're done loading, and bHasStarted is still true (meaning this is the first time we're encountering a request to complete)
// try and complete. It's entirely possible that a user callback might append new work, which they immediately start, which
// immediately tries to complete, which might create a case where we're now inside of TryCompleteAsyncLoading, which then
// calls Start, which then calls TryCompleteAsyncLoading, so when we come back out of the stack, we need to avoid trying to
// complete the async loading N+ times.
if (IsLoadingComplete() && bHasStarted)
{
CompleteAsyncLoading();
}
}
void FAsyncMixin::FLoadingState::CompleteAsyncLoading()
{
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] CompleteAsyncLoading"), this);
// Mark that we've completed loading.
if (bHasStarted)
{
bHasStarted = false;
OwnerRef.OnFinishedLoading();
}
// It's unlikely but possible they started loading more stuff in the OnFinishedLoading callback,
// so double check that we're still actually done.
//
// NOTE: We don't delete ourselves from memory in use. Doing things like
// pre-loading a bundle requires keeping the streaming handle alive. So we're keeping
// things alive.
//
// We won't destroy the memory but we need to cleanup anything that may be hanging on to
// captured scope, like completion handlers.
if (IsLoadingComplete())
{
if (!bPreloadedBundles && !IsLoadingInProgressOrPending())
{
// If we're all done loading or pending loading, we should clean up the memory we're using.
// go ahead and remove this loading state the owner mix-in allocated.
RequestDestroyThisMemory();
return;
}
}
}
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
FAsyncMixin::FLoadingState::FAsyncStep::FAsyncStep(const FSimpleDelegate& InUserCallback)
: UserCallback(InUserCallback)
{
}
FAsyncMixin::FLoadingState::FAsyncStep::FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr<FStreamableHandle>& InStreamingHandle)
: UserCallback(InUserCallback)
, StreamingHandle(InStreamingHandle)
{
}
FAsyncMixin::FLoadingState::FAsyncStep::FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr<FAsyncCondition>& InCondition)
: UserCallback(InUserCallback)
, Condition(InCondition)
{
}
FAsyncMixin::FLoadingState::FAsyncStep::~FAsyncStep()
{
}
void FAsyncMixin::FLoadingState::FAsyncStep::ExecuteUserCallback()
{
UserCallback.ExecuteIfBound();
UserCallback.Unbind();
}
bool FAsyncMixin::FLoadingState::FAsyncStep::IsComplete() const
{
if (StreamingHandle.IsValid())
{
return StreamingHandle->HasLoadCompleted();
}
else if (Condition.IsValid())
{
return Condition->IsComplete();
}
return true;
}
void FAsyncMixin::FLoadingState::FAsyncStep::Cancel()
{
if (StreamingHandle.IsValid())
{
StreamingHandle->BindCompleteDelegate(FSimpleDelegate());
StreamingHandle.Reset();
}
else if (Condition.IsValid())
{
Condition.Reset();
}
bIsCompletionDelegateBound = false;
}
bool FAsyncMixin::FLoadingState::FAsyncStep::BindCompleteDelegate(const FSimpleDelegate& NewDelegate)
{
if (IsComplete())
{
// Too Late!
return false;
}
if (StreamingHandle.IsValid())
{
StreamingHandle->BindCompleteDelegate(NewDelegate);
}
else if (Condition)
{
Condition->BindCompleteDelegate(NewDelegate);
}
bIsCompletionDelegateBound = true;
return true;
}
bool FAsyncMixin::FLoadingState::FAsyncStep::IsCompleteDelegateBound() const
{
return bIsCompletionDelegateBound;
}
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
FAsyncCondition::FAsyncCondition(const FAsyncConditionDelegate& Condition)
: UserCondition(Condition)
{
}
FAsyncCondition::FAsyncCondition(TFunction<EAsyncConditionResult()>&& Condition)
: UserCondition(FAsyncConditionDelegate::CreateLambda([UserFunction = MoveTemp(Condition)]() mutable { return UserFunction(); }))
{
}
FAsyncCondition::~FAsyncCondition()
{
FTSTicker::GetCoreTicker().RemoveTicker(RepeatHandle);
}
bool FAsyncCondition::IsComplete() const
{
if (UserCondition.IsBound())
{
const EAsyncConditionResult Result = UserCondition.Execute();
return Result == EAsyncConditionResult::Complete;
}
return true;
}
bool FAsyncCondition::BindCompleteDelegate(const FSimpleDelegate& NewDelegate)
{
if (IsComplete())
{
// Already Complete
return false;
}
CompletionDelegate = NewDelegate;
if (!RepeatHandle.IsValid())
{
RepeatHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateSP(this, &FAsyncCondition::TryToContinue), 0.16);
}
return true;
}
bool FAsyncCondition::TryToContinue(float)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FAsyncCondition_TryToContinue);
UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncCondition::TryToContinue"), this);
if (UserCondition.IsBound())
{
const EAsyncConditionResult Result = UserCondition.Execute();
switch (Result)
{
case EAsyncConditionResult::TryAgain:
return true;
case EAsyncConditionResult::Complete:
RepeatHandle.Reset();
UserCondition.Unbind();
CompletionDelegate.ExecuteIfBound();
CompletionDelegate.Unbind();
break;
}
}
return false;
}