// 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::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* LoadingState = Loading.Find(this)) { return (*LoadingState).Get(); } return Loading.Add(this, MakeShared(*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& SoftObjectPaths, const FSimpleDelegate& DelegateToCall) { GetLoadingState().AsyncLoad(SoftObjectPaths, DelegateToCall); } void FAsyncMixin::AsyncPreloadPrimaryAssetsAndBundles(const TArray& AssetIds, const TArray& LoadBundles, const FSimpleDelegate& DelegateToCall) { GetLoadingState().AsyncPreloadPrimaryAssetsAndBundles(AssetIds, LoadBundles, DelegateToCall); } void FAsyncMixin::AsyncCondition(TSharedRef 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& 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( DelegateToCall, UAssetManager::GetStreamableManager().RequestAsyncLoad(SoftObjectPath, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, false, false, TEXT("AsyncMixin")) ) ); TryScheduleStart(); } void FAsyncMixin::FLoadingState::AsyncLoad(const TArray& 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( DelegateToCall, UAssetManager::GetStreamableManager().RequestAsyncLoad(SoftObjectPaths, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, false, false, TEXT("AsyncMixin")) ) ); TryScheduleStart(); } void FAsyncMixin::FLoadingState::AsyncPreloadPrimaryAssetsAndBundles(const TArray& AssetIds, const TArray& 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 StreamingHandle; if (AssetIds.Num() > 0) { bPreloadedBundles = true; const bool bLoadRecursive = true; StreamingHandle = UAssetManager::Get().PreloadPrimaryAssets(AssetIds, LoadBundles, bLoadRecursive); } AsyncSteps.Add(MakeUnique(DelegateToCall, StreamingHandle)); TryScheduleStart(); } void FAsyncMixin::FLoadingState::AsyncCondition(TSharedRef Condition, const FSimpleDelegate& DelegateToCall) { UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncCondition '0x%X'"), this, &Condition.Get()); AsyncSteps.Add(MakeUnique(DelegateToCall, Condition)); TryScheduleStart(); } void FAsyncMixin::FLoadingState::AsyncEvent(const FSimpleDelegate& DelegateToCall) { UE_LOG(LogAsyncMixin, Verbose, TEXT("[0x%X] AsyncEvent"), this); AsyncSteps.Add(MakeUnique(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& InStreamingHandle) : UserCallback(InUserCallback) , StreamingHandle(InStreamingHandle) { } FAsyncMixin::FLoadingState::FAsyncStep::FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr& 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&& 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; }