RealtimeStyleTransferRuntime/Plugins/AsyncMixin/Source/Public/AsyncMixin.h

357 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/StrongObjectPtr.h"
#include "Engine/AssetManager.h"
#include "Async/Future.h"
#include "Containers/Ticker.h"
class FAsyncCondition;
class UPrimaryDataAsset;
DECLARE_DELEGATE_OneParam(FStreamableHandleDelegate, TSharedPtr<FStreamableHandle>)
//TODO I think we need to introduce a retention policy, preloads automatically stay in memory until canceled
// but what if you want to preload individual items just using the AsyncLoad functions? I don't want to
// introduce individual policies per call, or introduce a whole set of preload vs asyncloads, so would
// would rather have a retention policy. Should it be a member and actually create real memory when
// you inherit from AsyncMixin, or should it be a template argument?
//enum class EAsyncMixinRetentionPolicy : uint8
//{
// Default,
// KeepResidentUntilComplete,
// KeepResidentUntilCancel
//};
/**
* The FAsyncMixin allows easier management of async loading requests, to ensure linear request handling, to make
* writing code much easier. The usage pattern is as follows,
*
* First - inherit from FAsyncMixin, even if you're a UObject, you can also inherit from FAsyncMixin.
*
* Then - you can make your async loads as follows.
*
* CancelAsyncLoading(); // Some objects get reused like in lists, so it's important to cancel anything you had pending doesn't complete.
* AsyncLoad(ItemOne, CallbackOne);
* AsyncLoad(ItemTwo, CallbackTwo);
* StartAsyncLoading();
*
* You can also include the 'this' scope safely, one of the benefits of the mix-in, is that none of the callbacks
* are ever out of scope of the host AsyncMixin derived object.
* e.g.
* AsyncLoad(SomeSoftObjectPtr, [this, ...]() {
*
* });
*
*
* What will happen is first we cancel any existing one(s), e.g. perhaps we are a widget that just got told to represent
* some new thing. What will happen is we'll Load ItemOne and ItemTwo, *THEN* we'll call the callbacks in the order you
* requested the async loads - even if ItemOne or ItemTwo was already loaded when you request it.
*
* When all the async loading requests complete, OnFinishedLoading will be called.
*
* If you forget to call StartAsyncLoading(), we'll call it next frame, but you should remember to call it
* when you're done with your setup, as maybe everything is already loaded, and it will avoid a single frame
* of a loading indicator flash, which is annoying.
*
* NOTE: The FAsyncMixin also makes it safe to pass [this] as a captured input into your lambda, because it handles
* unhooking everything if either your owner class is destroyed, or you cancel everything.
*
* NOTE: FAsyncMixin doesn't add any additional memory to your class. Several classes currently handling async loading
* internally allocate TSharedPtr<FStreamableHandle> members and tend to hold onto SoftObjectPaths temporary state. The
* FAsyncMixin does all of this internally with a static TMap so that all of the async request memory is stored temporarily
* and sparsely.
*
* NOTE: For debugging and understanding what's going on, you should add -LogCmds="LogAsyncMixin Verbose" to the command line.
*/
class ASYNCMIXIN_API FAsyncMixin : public FNoncopyable
{
protected:
FAsyncMixin();
public:
virtual ~FAsyncMixin();
protected:
/** Called when loading starts. */
virtual void OnStartedLoading() { }
/** Called when all loading has finished. */
virtual void OnFinishedLoading() { }
protected:
/** Async load a TSoftClassPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftClassPtr<T> SoftClass, TFunction<void()>&& Callback)
{
AsyncLoad(SoftClass.ToSoftObjectPath(), FSimpleDelegate::CreateLambda(MoveTemp(Callback)));
}
/** Async load a TSoftClassPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftClassPtr<T> SoftClass, TFunction<void(TSubclassOf<T>)>&& Callback)
{
AsyncLoad(SoftClass.ToSoftObjectPath(),
FSimpleDelegate::CreateLambda([SoftClass, UserCallback = MoveTemp(Callback)]() mutable {
UserCallback(SoftClass.Get());
})
);
}
/** Async load a TSoftClassPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftClassPtr<T> SoftClass, const FSimpleDelegate& Callback = FSimpleDelegate())
{
AsyncLoad(SoftClass.ToSoftObjectPath(), Callback);
}
/** Async load a TSoftObjectPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftObjectPtr<T> SoftObject, TFunction<void()>&& Callback)
{
AsyncLoad(SoftObject.ToSoftObjectPath(), FSimpleDelegate::CreateLambda(MoveTemp(Callback)));
}
/** Async load a TSoftObjectPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftObjectPtr<T> SoftObject, TFunction<void(T*)>&& Callback)
{
AsyncLoad(SoftObject.ToSoftObjectPath(),
FSimpleDelegate::CreateLambda([SoftObject, UserCallback = MoveTemp(Callback)]() mutable {
UserCallback(SoftObject.Get());
})
);
}
/** Async load a TSoftObjectPtr<T>, call the Callback when complete. */
template<typename T = UObject>
void AsyncLoad(TSoftObjectPtr<T> SoftObject, const FSimpleDelegate& Callback = FSimpleDelegate())
{
AsyncLoad(SoftObject.ToSoftObjectPath(), Callback);
}
/** Async load a FSoftObjectPath, call the Callback when complete. */
void AsyncLoad(FSoftObjectPath SoftObjectPath, const FSimpleDelegate& Callback = FSimpleDelegate());
/** Async load an array of FSoftObjectPath, call the Callback when complete. */
void AsyncLoad(const TArray<FSoftObjectPath>& SoftObjectPaths, TFunction<void()>&& Callback)
{
AsyncLoad(SoftObjectPaths, FSimpleDelegate::CreateLambda(MoveTemp(Callback)));
}
/** Async load an array of FSoftObjectPath, call the Callback when complete. */
void AsyncLoad(const TArray<FSoftObjectPath>& SoftObjectPaths, const FSimpleDelegate& Callback = FSimpleDelegate());
/** Given an array of primary assets, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */
template<typename T = UPrimaryDataAsset>
void AsyncPreloadPrimaryAssetsAndBundles(const TArray<T*>& Assets, const TArray<FName>& LoadBundles, const FSimpleDelegate& Callback = FSimpleDelegate())
{
TArray<FPrimaryAssetId> PrimaryAssetIds;
for (const T* Item : Assets)
{
PrimaryAssetIds.Add(Item);
}
AsyncPreloadPrimaryAssetsAndBundles(PrimaryAssetIds, LoadBundles, Callback);
}
/** Given an array of primary asset ids, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */
void AsyncPreloadPrimaryAssetsAndBundles(const TArray<FPrimaryAssetId>& AssetIds, const TArray<FName>& LoadBundles, TFunction<void()>&& Callback)
{
AsyncPreloadPrimaryAssetsAndBundles(AssetIds, LoadBundles, FSimpleDelegate::CreateLambda(MoveTemp(Callback)));
}
/** Given an array of primary asset ids, it loads all of the bundles referenced by properties of these assets specified in the LoadBundles array. */
void AsyncPreloadPrimaryAssetsAndBundles(const TArray<FPrimaryAssetId>& AssetIds, const TArray<FName>& LoadBundles, const FSimpleDelegate& Callback = FSimpleDelegate());
/** Add a future condition that must be true before we move forward. */
void AsyncCondition(TSharedRef<FAsyncCondition> Condition, const FSimpleDelegate& Callback = FSimpleDelegate());
/**
* Rather than load anything, this callback is just inserted into the callback sequence so that when async loading
* completes this event will be called at the same point in the sequence. Super useful if you don't want a step to be
* tied to a particular asset in case some of the assets are optional.
*/
void AsyncEvent(TFunction<void()>&& Callback)
{
AsyncEvent(FSimpleDelegate::CreateLambda(MoveTemp(Callback)));
}
/**
* Rather than load anything, this callback is just inserted into the callback sequence so that when async loading
* completes this event will be called at the same point in the sequence. Super useful if you don't want a step to be
* tied to a particular asset in case some of the assets are optional.
*/
void AsyncEvent(const FSimpleDelegate& Callback);
/** Flushes any async loading requests. */
void StartAsyncLoading();
/** Cancels any pending async loads. */
void CancelAsyncLoading();
/** Is async loading current in progress? */
bool IsAsyncLoadingInProgress() const;
private:
/**
* The FLoadingState is what actually is allocated for the FAsyncMixin in a big map so that the FAsyncMixin itself holds no
* no memory, and we dynamically create the FLoadingState only if needed, and destroy it when it's unneeded.
*/
class FLoadingState : public TSharedFromThis<FLoadingState>
{
public:
FLoadingState(FAsyncMixin& InOwner);
virtual ~FLoadingState();
/** Starts the async sequence. */
void Start();
/** Cancels the async sequence. */
void CancelAndDestroy();
void AsyncLoad(FSoftObjectPath SoftObject, const FSimpleDelegate& DelegateToCall);
void AsyncLoad(const TArray<FSoftObjectPath>& SoftObjectPaths, const FSimpleDelegate& DelegateToCall);
void AsyncPreloadPrimaryAssetsAndBundles(const TArray<FPrimaryAssetId>& PrimaryAssetIds, const TArray<FName>& LoadBundles, const FSimpleDelegate& DelegateToCall);
void AsyncCondition(TSharedRef<FAsyncCondition> Condition, const FSimpleDelegate& Callback);
void AsyncEvent(const FSimpleDelegate& Callback);
bool IsLoadingComplete() const { return !IsLoadingInProgress(); }
bool IsLoadingInProgress() const;
bool IsLoadingInProgressOrPending() const;
bool IsPendingDestroy() const;
private:
void CancelOnly(bool bDestroying);
void CancelStartTimer();
void TryScheduleStart();
void TryCompleteAsyncLoading();
void CompleteAsyncLoading();
private:
void RequestDestroyThisMemory();
void CancelDestroyThisMemory(bool bDestroying);
/** Who owns the loading state? We need this to call back into the owning mix-in object. */
FAsyncMixin& OwnerRef;
/**
* Did we need to pre-load bundles? If we didn't pre-load bundles (which require you keep the streaming handle
* around or they will be destroyed), then we can safely destroy the FLoadingState when everything is done loading.
*/
bool bPreloadedBundles = false;
class FAsyncStep
{
public:
FAsyncStep(const FSimpleDelegate& InUserCallback);
FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr<FStreamableHandle>& InStreamingHandle);
FAsyncStep(const FSimpleDelegate& InUserCallback, const TSharedPtr<FAsyncCondition>& InCondition);
~FAsyncStep();
void ExecuteUserCallback();
bool IsLoadingInProgress() const
{
return !IsComplete();
}
bool IsComplete() const;
void Cancel();
bool BindCompleteDelegate(const FSimpleDelegate& NewDelegate);
bool IsCompleteDelegateBound() const;
private:
FSimpleDelegate UserCallback;
bool bIsCompletionDelegateBound = false;
// Possible Async 'thing'
TSharedPtr<FStreamableHandle> StreamingHandle;
TSharedPtr<FAsyncCondition> Condition;
};
bool bHasStarted = false;
int32 CurrentAsyncStep = 0;
TArray<TUniquePtr<FAsyncStep>> AsyncSteps;
TArray<TUniquePtr<FAsyncStep>> AsyncStepsPendingDestruction;
FTSTicker::FDelegateHandle StartTimerDelegate;
FTSTicker::FDelegateHandle DestroyMemoryDelegate;
};
const FLoadingState& GetLoadingStateConst() const;
FLoadingState& GetLoadingState();
bool HasLoadingState() const;
bool IsLoadingInProgressOrPending() const;
private:
static TMap<FAsyncMixin*, TSharedRef<FLoadingState>> Loading;
};
/**
* Sometimes a mix-in just doesn't make sense. Perhaps the object has to manage many different jobs
* that each have their own async dependency chain/scope. For those situations you can use the FAsyncScope.
*
* This class is a standalone Async dependency handler so that you can fire off several load jobs and always handle them
* in the proper order, just like with combining FAsyncMixin with your class.
*/
class ASYNCMIXIN_API FAsyncScope : public FAsyncMixin
{
public:
using FAsyncMixin::AsyncLoad;
using FAsyncMixin::AsyncPreloadPrimaryAssetsAndBundles;
using FAsyncMixin::AsyncCondition;
using FAsyncMixin::AsyncEvent;
using FAsyncMixin::CancelAsyncLoading;
using FAsyncMixin::StartAsyncLoading;
using FAsyncMixin::IsAsyncLoadingInProgress;
};
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
enum class EAsyncConditionResult : uint8
{
TryAgain,
Complete
};
DECLARE_DELEGATE_RetVal(EAsyncConditionResult, FAsyncConditionDelegate);
/**
* The async condition allows you to have custom reasons to hault the async loading until some condition is met.
*/
class FAsyncCondition : public TSharedFromThis<FAsyncCondition>
{
public:
FAsyncCondition(const FAsyncConditionDelegate& Condition);
FAsyncCondition(TFunction<EAsyncConditionResult()>&& Condition);
virtual ~FAsyncCondition();
protected:
bool IsComplete() const;
bool BindCompleteDelegate(const FSimpleDelegate& NewDelegate);
private:
bool TryToContinue(float DeltaTime);
FTSTicker::FDelegateHandle RepeatHandle;
FAsyncConditionDelegate UserCondition;
FSimpleDelegate CompletionDelegate;
friend FAsyncMixin;
};