diff --git a/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp b/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp index a71b48c..18e22e4 100644 --- a/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp +++ b/Source/StevesUEHelpers/Private/StevesGameSubsystem.cpp @@ -249,6 +249,26 @@ void UStevesGameSubsystem::SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterfac } } +FStevesTextureRenderTargetPoolPtr UStevesGameSubsystem::GetTextureRenderTargetPool(FName Name, bool bAutoCreate) +{ + // On the assumption there won't be *loads* of pools, not worth a map, just iterate + for (auto Tex : TextureRenderTargetPools) + { + if (Tex->GetName() == Name) + return Tex; + } + + if (bAutoCreate) + { + FStevesTextureRenderTargetPoolPtr Pool = MakeShared(Name, this); + TextureRenderTargetPools.Add(Pool); + return Pool; + } + + return nullptr; + +} + bool UStevesGameSubsystem::FInputModeDetector::ShouldProcessInputEvents() const { diff --git a/Source/StevesUEHelpers/Private/StevesTextureRenderTargetPool.cpp b/Source/StevesUEHelpers/Private/StevesTextureRenderTargetPool.cpp new file mode 100644 index 0000000..6099c20 --- /dev/null +++ b/Source/StevesUEHelpers/Private/StevesTextureRenderTargetPool.cpp @@ -0,0 +1,100 @@ +#include "StevesTextureRenderTargetPool.h" + +#include "StevesUEHelpers.h" +#include "Kismet/KismetRenderingLibrary.h" + +FStevesTextureRenderTargetReservation::~FStevesTextureRenderTargetReservation() +{ + UE_LOG(LogStevesUEHelpers, Log, TEXT("FStevesTextureRenderTargetReservation: destruction")); + if (ParentPool.IsValid() && Texture.IsValid()) + { + ParentPool.Pin()->ReleaseReservation(Texture.Get()); + Texture = nullptr; + } +} + +void FStevesTextureRenderTargetPool::ReleaseReservation(UTextureRenderTarget2D* Tex) +{ + if (!Tex) + { + UE_LOG(LogStevesUEHelpers, Warning, TEXT("FStevesTextureRenderTargetPool: Attempted to release a null texture")); + return; + } + + for (int i = 0; i < Reservations.Num(); ++i) + { + const FReservationInfo& R = Reservations[i]; + if (R.Texture.IsValid() && R.Texture.Get() == Tex) + { + UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Released texture reservation on %s"), *Tex->GetName()); + UnreservedTextures.Add(R.Key, Tex); + Reservations.RemoveAtSwap(i); + return; + } + } + + UE_LOG(LogStevesUEHelpers, Warning, TEXT("FStevesTextureRenderTargetPool: Attempted to release a reservation on %s that was not found"), *Tex->GetName()); + +} + +FStevesTextureRenderTargetReservationPtr FStevesTextureRenderTargetPool::ReserveTexture(FIntPoint Size, + ETextureRenderTargetFormat Format, const UObject* Owner) +{ + const FTextureKey Key {Size, Format}; + UTextureRenderTarget2D* Tex = nullptr; + if (auto Pooled = UnreservedTextures.Find(Key)) + { + Tex = *Pooled; + UnreservedTextures.RemoveSingle(Key, Tex); + UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Re-used pooled texture %s"), *Tex->GetName()); + } + else if (Size.X > 0 && Size.Y > 0) + { + // No existing texture, so create + // Texture owner should be a valid UObject that will determine lifespan + UObject* TextureOwner = PoolOwner.IsValid() ? PoolOwner.Get() : GetTransientPackage(); + Tex = NewObject(TextureOwner); + Tex->RenderTargetFormat = Format; + Tex->InitAutoFormat(Size.X, Size.Y); + Tex->UpdateResourceImmediate(true); + + UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Created new texture %s"), *Tex->GetName()); + } + + // Record reservation + Reservations.Add(FReservationInfo(Key, Owner, Tex)); + + return MakeShared(Tex, this->AsShared(), Owner); +} + +void FStevesTextureRenderTargetPool::RevokeReservations(const UObject* ForOwner) +{ + for (int i = 0; i < Reservations.Num(); ++i) + { + const FReservationInfo& R = Reservations[i]; + if (!ForOwner || R.Owner == ForOwner) + { + if (R.Texture.IsValid()) + { + UE_LOG(LogStevesUEHelpers, Verbose, TEXT("FStevesTextureRenderTargetPool: Revoked texture reservation on %s"), *R.Texture->GetName()); + UnreservedTextures.Add(R.Key, R.Texture.Get()); + } + // Can't use RemoveAtSwap because it'll change order + Reservations.RemoveAt(i); + // Adjust index backwards to compensate + --i; + } + } +} + +void FStevesTextureRenderTargetPool::DrainPool(bool bForceAndRevokeReservations) +{ + if (bForceAndRevokeReservations) + RevokeReservations(); + + for (auto& TexPair : UnreservedTextures) + { + UKismetRenderingLibrary::ReleaseRenderTarget2D(TexPair.Value); + } + UnreservedTextures.Empty(); +} diff --git a/Source/StevesUEHelpers/Public/StevesGameSubsystem.h b/Source/StevesUEHelpers/Public/StevesGameSubsystem.h index 5ad56ce..d7df976 100644 --- a/Source/StevesUEHelpers/Public/StevesGameSubsystem.h +++ b/Source/StevesUEHelpers/Public/StevesGameSubsystem.h @@ -6,6 +6,7 @@ #include "PaperSprite.h" #include "Framework/Application/IInputProcessor.h" #include "StevesHelperCommon.h" +#include "StevesTextureRenderTargetPool.h" #include "StevesUI/FocusSystem.h" #include "StevesUI/UiTheme.h" @@ -14,6 +15,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInputModeChanged, int, PlayerIndex, EInputMode, InputMode); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnWindowForegroundChanged, bool, bFocussed); +/// Entry point for all the top-level features of the helper system UCLASS(Config=Game) class STEVESUEHELPERS_API UStevesGameSubsystem : public UGameInstanceSubsystem { @@ -109,6 +111,8 @@ protected: UPROPERTY(BlueprintReadWrite) UUiTheme* DefaultUiTheme; + TArray TextureRenderTargetPools; + void CreateInputDetector(); void DestroyInputDetector(); void InitTheme(); @@ -198,4 +202,14 @@ public: static void SetBrushFromAtlas(FSlateBrush* Brush, TScriptInterface AtlasRegion, bool bMatchSize); + + + /** + * Retrieve a pool of texture render targets. If a pool doesn't exist with the given name, it can be created. + * @param Name Identifier for the pool. + * @param bAutoCreate + * @return The pool, or null if it doesn't exist and bAutoCreate is false + */ + FStevesTextureRenderTargetPoolPtr GetTextureRenderTargetPool(FName Name, bool bAutoCreate = true); + }; diff --git a/Source/StevesUEHelpers/Public/StevesTextureRenderTargetPool.h b/Source/StevesUEHelpers/Public/StevesTextureRenderTargetPool.h new file mode 100644 index 0000000..c837591 --- /dev/null +++ b/Source/StevesUEHelpers/Public/StevesTextureRenderTargetPool.h @@ -0,0 +1,138 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Chaos/AABB.h" +#include "Chaos/AABB.h" +#include "Engine/TextureRenderTarget2D.h" + +typedef TSharedPtr FStevesTextureRenderTargetReservationPtr; +typedef TSharedPtr FStevesTextureRenderTargetPoolPtr; + +/// Holder for an assigned texture. While this structure exists, the texture will be considered assigned +/// and will not be returned from any other request. Once this structure is destroyed the texture will +/// be free for re-use. For that reason, only pass this structure around by SharedRef/SharedPtr. +/// The texture is held by a weak pointer however, the strong pointer is held by the pool. The texture will continue +/// to be available to this reservation except if the pool is told to forcibly release textures. +struct STEVESUEHELPERS_API FStevesTextureRenderTargetReservation +{ +public: + /// The texture. May be null if the pool has forcibly reclaimed the texture prematurely + TWeakObjectPtr Texture; + TWeakPtr ParentPool; + TWeakObjectPtr CurrentOwner; + + FStevesTextureRenderTargetReservation() = default; + + FStevesTextureRenderTargetReservation(UTextureRenderTarget2D* InTexture, + FStevesTextureRenderTargetPoolPtr InParent, + const UObject* InOwner) + : Texture(InTexture), + ParentPool(InParent), + CurrentOwner(InOwner) + + { + } + + ~FStevesTextureRenderTargetReservation(); +}; + + +/** + * A pool of render target textures. To save pre-creating render textures as assets, and to control the re-use of + * these textures at runtime. + * A pool needs to be owned by a UObject, which will in turn own the textures and so will ultimately control the + * ultimate lifecycle of textures if not released specifically. + */ +struct STEVESUEHELPERS_API FStevesTextureRenderTargetPool : public TSharedFromThis +{ + +protected: + /// The name of the pool. It's possible to have more than one texture pool. + FName Name; + + struct FTextureKey + { + FIntPoint Size; + ETextureRenderTargetFormat Format; + + friend bool operator==(const FTextureKey& Lhs, const FTextureKey& RHS) + { + return Lhs.Size == RHS.Size + && Lhs.Format == RHS.Format; + } + + friend bool operator!=(const FTextureKey& Lhs, const FTextureKey& RHS) + { + return !(Lhs == RHS); + } + + friend uint32 GetTypeHash(const FTextureKey& Key) + { + return HashCombine(GetTypeHash(Key.Size), static_cast(Key.Format)); + } + + }; + + TWeakObjectPtr PoolOwner; + TMultiMap UnreservedTextures; + + /// Weak reverse tracking of reservations, mostly for debugging + struct FReservationInfo + { + FTextureKey Key; + TWeakObjectPtr Owner; + TWeakObjectPtr Texture; + + FReservationInfo(const FTextureKey& InKey, const UObject* InOwner, UTextureRenderTarget2D* InTexture) + : Key(InKey), + Owner(InOwner), + Texture(InTexture) + { + } + }; + TArray Reservations; + + + friend struct FStevesTextureRenderTargetReservation; + /// Release a reservation on a texture, allowing it back into the pool + /// Protected because only FStevesTextureRenderTargetReservation will need to do this. + void ReleaseReservation(UTextureRenderTarget2D* Tex); + +public: + + explicit FStevesTextureRenderTargetPool(const FName& InName, UObject* InOwner) + : Name(InName), PoolOwner(InOwner) + { + } + + const FName& GetName() const { return Name; } + + /** + * Reserve a texture for use as a render target. This will create a new texture target if needed. + * @param Size The dimensions of the texture + * @param Format Format of the texture + * @param Owner The UObject which will temporarily own this texture (mostly for debugging, this object won't in fact "own" it + * as per garbage collection rules, the reference is weak + * @return A shared pointer to a structure which holds the reservation for this texture. When that structure is + * destroyed, it will release the texture back to the pool. + */ + FStevesTextureRenderTargetReservationPtr ReserveTexture(FIntPoint Size, ETextureRenderTargetFormat Format, const UObject* Owner); + + /** + * Forcibly revoke reservations in this pool, either for all owners or for a specific owner. + * Reservations which are revoked will have their weak texture pointers invalidated. + * @param ForOwner If null, revoke all reservations for any owner, or if provided, just for a specific owner. + */ + void RevokeReservations(const UObject* ForOwner = nullptr); + + + /** + * + * Destroy previously created textures and free the memory. + * @param bForceAndRevokeReservations If false, only destroys unreserved textures. If true, destroys reserved textures + * as well (the weak pointer on their reservations will cease to be valid) + */ + void DrainPool(bool bForceAndRevokeReservations = false); + +}; +