diff --git a/Source/ImGui/Private/ImGuiModule.cpp b/Source/ImGui/Private/ImGuiModule.cpp index e33dc3b..90efc75 100644 --- a/Source/ImGui/Private/ImGuiModule.cpp +++ b/Source/ImGui/Private/ImGuiModule.cpp @@ -3,6 +3,9 @@ #include "ImGuiPrivatePCH.h" #include "ImGuiModuleManager.h" + +#include "ImGuiTextureHandle.h" +#include "TextureManager.h" #include "Utilities/WorldContext.h" #include "Utilities/WorldContextIndex.h" @@ -113,6 +116,26 @@ void FImGuiModule::RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle) } } +FImGuiTextureHandle FImGuiModule::FindTextureHandle(const FName& Name) +{ + const TextureIndex Index = ImGuiModuleManager->GetTextureManager().FindTextureIndex(Name); + return (Index != INDEX_NONE) ? FImGuiTextureHandle{ Name, ImGuiInterops::ToImTextureID(Index) } : FImGuiTextureHandle{}; +} + +FImGuiTextureHandle FImGuiModule::RegisterTexture(const FName& Name, class UTexture2D* Texture, bool bMakeUnique) +{ + const TextureIndex Index = ImGuiModuleManager->GetTextureManager().CreateTextureResources(Name, Texture, bMakeUnique); + return FImGuiTextureHandle{ Name, ImGuiInterops::ToImTextureID(Index) }; +} + +void FImGuiModule::ReleaseTexture(const FImGuiTextureHandle& Handle) +{ + if (Handle.IsValid()) + { + ImGuiModuleManager->GetTextureManager().ReleaseTextureResources(ImGuiInterops::ToTextureIndex(Handle.GetTextureId())); + } +} + void FImGuiModule::StartupModule() { // Create managers that implements module logic. @@ -198,6 +221,17 @@ void FImGuiModule::ToggleShowDemo() SetShowDemo(!IsShowingDemo()); } +//---------------------------------------------------------------------------------------------------- +// Partial implementations of other classes that needs access to ImGuiModuleManager +//---------------------------------------------------------------------------------------------------- + +bool FImGuiTextureHandle::HasValidEntry() const +{ + const TextureIndex Index = ImGuiInterops::ToTextureIndex(TextureId); + return Index != INDEX_NONE && ImGuiModuleManager && ImGuiModuleManager->GetTextureManager().GetTextureName(Index) == Name; +} + + #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FImGuiModule, ImGui) diff --git a/Source/ImGui/Private/ImGuiModuleManager.cpp b/Source/ImGui/Private/ImGuiModuleManager.cpp index 8ffa94c..a0e9174 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.cpp +++ b/Source/ImGui/Private/ImGuiModuleManager.cpp @@ -62,6 +62,8 @@ void FImGuiModuleManager::LoadTextures() { bTexturesLoaded = true; + TextureManager.InitializeErrorTexture(FColor::Magenta); + // Create an empty texture at index 0. We will use it for ImGui outputs with null texture id. TextureManager.CreatePlainTexture(FName{ "ImGuiModule_Plain" }, 2, 2, FColor::White); diff --git a/Source/ImGui/Private/ImGuiTextureHandle.cpp b/Source/ImGui/Private/ImGuiTextureHandle.cpp new file mode 100644 index 0000000..9103379 --- /dev/null +++ b/Source/ImGui/Private/ImGuiTextureHandle.cpp @@ -0,0 +1,26 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#include "ImGuiPrivatePCH.h" + +#include "ImGuiTextureHandle.h" + +#include "ImGuiInteroperability.h" + + +FImGuiTextureHandle::FImGuiTextureHandle() + : FImGuiTextureHandle(NAME_None, ImGuiInterops::ToImTextureID(INDEX_NONE)) +{ +} + +FImGuiTextureHandle::FImGuiTextureHandle(const FName& InName, ImTextureID InTextureId) + : Name(InName) + , TextureId(InTextureId) +{ + const TextureIndex Index = ImGuiInterops::ToTextureIndex(TextureId); + checkf((Index == INDEX_NONE) == (Name == NAME_None), + TEXT("Mismatch between Name and TextureId parameters, with only one indicating a null handle.") + TEXT(" Name = '%s', TextureIndex(TextureId) = %d"), *Name.ToString(), Index); +} + +// FImGuiTextureHandle::HasValidEntry() is implemented in ImGuiModule.cpp to get access to FImGuiModuleManager instance +// without referencing in this class. diff --git a/Source/ImGui/Private/TextureManager.cpp b/Source/ImGui/Private/TextureManager.cpp index c165ed0..405cad1 100644 --- a/Source/ImGui/Private/TextureManager.cpp +++ b/Source/ImGui/Private/TextureManager.cpp @@ -7,10 +7,51 @@ #include +void FTextureManager::InitializeErrorTexture(const FColor& Color) +{ + CreatePlainTextureInternal(NAME_ErrorTexture, 2, 2, Color); +} + TextureIndex FTextureManager::CreateTexture(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup) { - checkf(FindTextureIndex(Name) == INDEX_NONE, TEXT("Trying to create texture using resource name '%s' that is already registered."), *Name.ToString()); + checkf(Name != NAME_None, TEXT("Trying to create a texture with a name 'NAME_None' is not allowed.")); + checkf(FindTextureIndex(Name) == INDEX_NONE, TEXT("Trying to create texture using name '%s' that is already registered."), *Name.ToString()); + return CreateTextureInternal(Name, Width, Height, SrcBpp, SrcData, SrcDataCleanup); +} + +TextureIndex FTextureManager::CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color) +{ + checkf(Name != NAME_None, TEXT("Trying to create a texture with a name 'NAME_None' is not allowed.")); + checkf(FindTextureIndex(Name) == INDEX_NONE, TEXT("Trying to create texture using name '%s' that is already registered."), *Name.ToString()); + + return CreatePlainTextureInternal(Name, Width, Height, Color); +} + +TextureIndex FTextureManager::CreateTextureResources(const FName& Name, UTexture2D* Texture, bool bMakeUnique) +{ + checkf(Name != NAME_None, TEXT("Trying to create texture resources with a name 'NAME_None' is not allowed.")); + checkf(Texture, TEXT("Null Texture.")); + + if (bMakeUnique) + { + checkf(FindTextureIndex(Name) == INDEX_NONE, TEXT("Trying to create texture resources using name '%s' that is already registered.") + TEXT(" Consider using different name or set bMakeUnique parameter to false."), *Name.ToString()); + } + + // Create an entry for the texture. + return AddTextureEntry(Name, Texture, false, true); +} + +void FTextureManager::ReleaseTextureResources(TextureIndex Index) +{ + checkf(IsInRange(Index), TEXT("Invalid texture index %d. Texture resources array has %d entries total."), Index, TextureResources.Num()); + + TextureResources[Index] = {}; +} + +TextureIndex FTextureManager::CreateTextureInternal(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup) +{ // Create a texture. UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height); @@ -26,11 +67,19 @@ TextureIndex FTextureManager::CreateTexture(const FName& Name, int32 Width, int3 }; Texture->UpdateTextureRegions(0, 1u, TextureRegion, SrcBpp * Width, SrcBpp, SrcData, DataCleanup); - // Create a new entry for the texture. - return TextureResources.Emplace(Name, Texture); + // Create an entry for the texture. + if (Name == NAME_ErrorTexture) + { + ErrorTexture = { Name, Texture, true }; + return INDEX_ErrorTexture; + } + else + { + return AddTextureEntry(Name, Texture, true, false); + } } -TextureIndex FTextureManager::CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color) +TextureIndex FTextureManager::CreatePlainTextureInternal(const FName& Name, int32 Width, int32 Height, const FColor& Color) { // Create buffer with raw data. const uint32 ColorPacked = Color.ToPackedARGB(); @@ -42,15 +91,44 @@ TextureIndex FTextureManager::CreatePlainTexture(const FName& Name, int32 Width, auto SrcDataCleanup = [](uint8* Data) { delete[] Data; }; // Create new texture from raw data. - return CreateTexture(Name, Width, Height, Bpp, SrcData, SrcDataCleanup); + return CreateTextureInternal(Name, Width, Height, Bpp, SrcData, SrcDataCleanup); } -FTextureManager::FTextureEntry::FTextureEntry(const FName& InName, UTexture2D* InTexture) - : Name{ InName } - , Texture{ InTexture } +TextureIndex FTextureManager::AddTextureEntry(const FName& Name, UTexture2D* Texture, bool bAddToRoot, bool bUpdate) { - // Add texture to root to prevent garbage collection. - Texture->AddToRoot(); + // If we update try to find entry with that name. + TextureIndex Index = bUpdate ? FindTextureIndex(Name) : INDEX_NONE; + + // If we didn't find, try to find and entry to reuse. + if (Index == INDEX_NONE) + { + Index = FindTextureIndex(NAME_None); + } + + // Either update/reuse entry or add a new one. + if (Index != INDEX_NONE) + { + TextureResources[Index] = { Name, Texture, bAddToRoot }; + return Index; + } + else + { + return TextureResources.Emplace(Name, Texture, bAddToRoot); + } +} + +FTextureManager::FTextureEntry::FTextureEntry(const FName& InName, UTexture2D* InTexture, bool bAddToRoot) + : Name(InName) +{ + checkf(InTexture, TEXT("Null texture.")); + + if (bAddToRoot) + { + // Get pointer only for textures that we added to root, so we can later release them. + Texture = InTexture; + // Add texture to the root to prevent garbage collection. + InTexture->AddToRoot(); + } // Create brush and resource handle for input texture. Brush.SetResourceObject(InTexture); @@ -59,16 +137,50 @@ FTextureManager::FTextureEntry::FTextureEntry(const FName& InName, UTexture2D* I FTextureManager::FTextureEntry::~FTextureEntry() { - // Release brush. - if (Brush.HasUObject() && FSlateApplication::IsInitialized()) + Reset(true); +} + +FTextureManager::FTextureEntry& FTextureManager::FTextureEntry::operator=(FTextureEntry&& Other) +{ + // Release old resources if allocated. + Reset(true); + + // Move data and ownership to this instance. + Name = MoveTemp(Other.Name); + Texture = MoveTemp(Other.Texture); + Brush = MoveTemp(Other.Brush); + ResourceHandle = MoveTemp(Other.ResourceHandle); + + // Reset the other entry (without releasing resources which are already moved to this instance) to remove tracks + // of ownership and mark it as empty/reusable. + Other.Reset(false); + + return *this; +} + +void FTextureManager::FTextureEntry::Reset(bool bReleaseResources) +{ + if (bReleaseResources) { - FSlateApplication::Get().GetRenderer()->ReleaseDynamicResource(Brush); + // Release brush. + if (Brush.HasUObject() && FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReleaseDynamicResource(Brush); + } + + // Remove texture from root to allow for garbage collection (it might be invalid, if we never set it + // or this is an application shutdown). + if (Texture.IsValid()) + { + Texture->RemoveFromRoot(); + } } - // Remove texture from root to allow for garbage collection (it might be already invalid if this is application - // shutdown). - if (Texture.IsValid()) - { - Texture->RemoveFromRoot(); - } + // We use empty name to mark unused entries. + Name = NAME_None; + + // Clean fields to make sure that we don't reference released or moved resources. + Texture.Reset(); + Brush = FSlateNoResource(); + ResourceHandle = FSlateResourceHandle(); } diff --git a/Source/ImGui/Private/TextureManager.h b/Source/ImGui/Private/TextureManager.h index 2c06896..73258d1 100644 --- a/Source/ImGui/Private/TextureManager.h +++ b/Source/ImGui/Private/TextureManager.h @@ -27,6 +27,13 @@ public: FTextureManager(FTextureManager&&) = default; FTextureManager& operator=(FTextureManager&&) = default; + // Initialize error texture that will be used for rendering textures without registered resources. Can be called + // multiple time, if color needs to be changed. + // Note: Because of any-time module loading and lazy resources initialization goals we can't simply call it from + // constructor. + // @param Color - The color of the error texture + void InitializeErrorTexture(const FColor& Color); + // Find texture index by name. // @param Name - The name of a texture to find // @returns The index of a texture with given name or INDEX_NONE if there is no such texture @@ -35,20 +42,22 @@ public: return TextureResources.IndexOfByPredicate([&](const auto& Entry) { return Entry.Name == Name; }); } - // Get the name of a texture at given index. Throws exception if index is out of range. + // Get the name of a texture at given index. Returns NAME_None, if index is out of range. // @param Index - Index of a texture - // @returns The name of a texture at given index - FORCEINLINE FName GetTextureName(TextureIndex Index) const + // @returns The name of a texture at given index or NAME_None if index is out of range. + FName GetTextureName(TextureIndex Index) const { - return TextureResources[Index].Name; + return IsInRange(Index) ? TextureResources[Index].Name : NAME_None; } - // Get the Slate Resource Handle to a texture at given index. Throws exception if index is out of range. + // Get the Slate Resource Handle to a texture at given index. If index is out of range or resources are not valid + // it returns a handle to the error texture. // @param Index - Index of a texture - // @returns The Slate Resource Handle for a texture at given index - FORCEINLINE const FSlateResourceHandle& GetTextureHandle(TextureIndex Index) const + // @returns The Slate Resource Handle for a texture at given index or to error texture, if no valid resources were + // found at given index + const FSlateResourceHandle& GetTextureHandle(TextureIndex Index) const { - return TextureResources[Index].ResourceHandle; + return IsValidTexture(Index) ? TextureResources[Index].ResourceHandle : ErrorTexture.ResourceHandle; } // Create a texture from raw data. Throws exception if there is already a texture with that name. @@ -69,27 +78,81 @@ public: // @returns The index of a texture that was created TextureIndex CreatePlainTexture(const FName& Name, int32 Width, int32 Height, FColor Color); + // Create Slate resources to an existing texture, managed externally. As part of an external interface it allows + // to loosen resource verification policy. By default (consistently with other create function) it protects from + // creating resources with name that is already registered. If bMakeUnique is false, then existing resources are + // updated/replaced. Throws exception, if name argument is NAME_None or texture is null. + // @param Name - The texture name + // @param Texture - The texture + // @param bMakeUnique - If true (default) and there is already a texture with given name, then exception is thrown, + // otherwise existing resources are updated. + // @returns The index to created/updated texture resources + TextureIndex CreateTextureResources(const FName& Name, UTexture2D* Texture, bool bMakeUnique = true); + + // Release resources for given texture. Ignores invalid indices. + // @param Index - The index of a texture resources + void ReleaseTextureResources(TextureIndex Index); + private: + // See CreateTexture for general description. + // Internal implementations doesn't validate name or resource uniqueness. Instead it uses NAME_ErrorTexture + // (aka NAME_None) and INDEX_ErrorTexture (aka INDEX_NONE) to identify ErrorTexture. + TextureIndex CreateTextureInternal(const FName& Name, int32 Width, int32 Height, uint32 SrcBpp, uint8* SrcData, TFunction SrcDataCleanup = [](uint8*) {}); + + // See CreatePlainTexture for general description. + // Internal implementations doesn't validate name or resource uniqueness. Instead it uses NAME_ErrorTexture + // (aka NAME_None) and INDEX_ErrorTexture (aka INDEX_NONE) to identify ErrorTexture. + TextureIndex CreatePlainTextureInternal(const FName& Name, int32 Width, int32 Height, const FColor& Color); + + // Add or reuse texture entry. + // @param Name - The texture name + // @param Texture - The texture + // @param bAddToRoot - If true, we should add texture to root to prevent garbage collection (use for own textures) + // @returns The index of the entry that we created or reused + TextureIndex AddTextureEntry(const FName& Name, UTexture2D* Texture, bool bAddToRoot, bool bUpdate); + + // Check whether index is in range allocated for TextureResources (it doesn't mean that resources are valid). + FORCEINLINE bool IsInRange(TextureIndex Index) const + { + return static_cast(Index) < static_cast(TextureResources.Num()); + } + + // Check whether index is in range and whether texture resources are valid (using NAME_None sentinel). + FORCEINLINE bool IsValidTexture(TextureIndex Index) const + { + return IsInRange(Index) && TextureResources[Index].Name != NAME_None; + } + // Entry for texture resources. Only supports explicit construction. struct FTextureEntry { - FTextureEntry(const FName& InName, UTexture2D* InTexture); + FTextureEntry() = default; + FTextureEntry(const FName& InName, UTexture2D* InTexture, bool bAddToRoot); ~FTextureEntry(); // Copying is not supported. FTextureEntry(const FTextureEntry&) = delete; FTextureEntry& operator=(const FTextureEntry&) = delete; - // We rely on TArray and don't implement custom move semantics. + // We rely on TArray and don't implement custom move constructor... FTextureEntry(FTextureEntry&&) = delete; - FTextureEntry& operator=(FTextureEntry&&) = delete; + // ... but we need move assignment to support reusing entries. + FTextureEntry& operator=(FTextureEntry&& Other); FName Name = NAME_None; TWeakObjectPtr Texture; FSlateBrush Brush; FSlateResourceHandle ResourceHandle; + + private: + + void Reset(bool bReleaseResources); }; TArray TextureResources; + FTextureEntry ErrorTexture; + + static constexpr EName NAME_ErrorTexture = NAME_None; + static constexpr TextureIndex INDEX_ErrorTexture = INDEX_NONE; }; diff --git a/Source/ImGui/Public/ImGuiModule.h b/Source/ImGui/Public/ImGuiModule.h index 0fd4a4d..1a261ca 100644 --- a/Source/ImGui/Public/ImGuiModule.h +++ b/Source/ImGui/Public/ImGuiModule.h @@ -3,6 +3,7 @@ #pragma once #include "ImGuiDelegates.h" +#include "ImGuiTextureHandle.h" #include @@ -69,6 +70,39 @@ public: */ virtual void RemoveImGuiDelegate(const FImGuiDelegateHandle& Handle); + /** + * If it exists, get a handle to the texture with given resource name. + * + * @param Name - Resource name of a texture to find + * @returns Handle to a registered texture or invalid handle if resources could not be found or were not valid + */ + virtual FImGuiTextureHandle FindTextureHandle(const FName& Name); + + /** + * Register texture and create its Slate resources. If texture with that name already exists then it may be updated + * or if bMakeUnique is true, exception will be thrown. Throws exception, if name argument is NAME_None or texture + * is null. + * + * Note, that updating texture resources doesn't invalidate already existing handles and returned handle will have + * the same value. + * + * @param Name - Resource name for the texture that needs to be registered or updated + * @param Texture - Texture for which we want to create or update Slate resources + * @param bMakeUnique - If false then existing resources are updated/overwritten (default). If true, then stricter + * policy is applied and if resource with that name exists then exception is thrown. + * @returns Handle to the texture resources, which can be used to release allocated resources and as an argument to + * relevant ImGui functions + */ + virtual FImGuiTextureHandle RegisterTexture(const FName& Name, class UTexture2D* Texture, bool bMakeUnique = false); + + /** + * Unregister texture and release its Slate resources. If handle is null or not valid, this function fails silently + * (for definition of 'valid' look @ FImGuiTextureHandle). + * + * @returns ImGui Texture Handle to texture that needs to be unregistered + */ + virtual void ReleaseTexture(const FImGuiTextureHandle& Handle); + /** * Check whether Input Mode is enabled (tests ImGui.InputEnabled console variable). * diff --git a/Source/ImGui/Public/ImGuiTextureHandle.h b/Source/ImGui/Public/ImGuiTextureHandle.h new file mode 100644 index 0000000..c67cf31 --- /dev/null +++ b/Source/ImGui/Public/ImGuiTextureHandle.h @@ -0,0 +1,64 @@ +// Distributed under the MIT License (MIT) (see accompanying LICENSE file) + +#pragma once + +#include + +#include + + +/** + * Handle to texture resources registered in module instance. Returned after successful texture registration. + * Can be implicitly converted to ImTextureID making it possible to use it directly with ImGui interface. + * Once texture is not needed handle can be used to release resources. + */ +class IMGUI_API FImGuiTextureHandle +{ +public: + + /** Creates an empty (null and not valid) texture handle. */ + FImGuiTextureHandle(); + + /** + * Checks whether this handle is null. Can be used as a quick test whether it points to any resources but it does + * not check whether those resources are valid (see @ IsValid). + * + * @returns True, if this handle is null (Name is NAME_None and TextureId is invalid) and false otherwise. + */ + bool IsNull() const { return Name == NAME_None; } + + /** + * Checks whether this handle is not null and valid. Valid handle points to valid texture resources. + * It is slower but safer test, more useful when there is no guarantee that resources haven't been released. + * + * @returns True, if this handle is not null and valid, false otherwise. + */ + bool IsValid() const { return !IsNull() && HasValidEntry(); } + + /** Get the name of the texture resources (NAME_None if handle is null). */ + const FName& GetName() const { return Name; } + + /** Get the ImGui texture id for this texture (invalid if handle is null). */ + ImTextureID GetTextureId() const { return TextureId; } + + /** Implicit conversion to ImTextureID. */ + operator ImTextureID() const { return GetTextureId(); } + +private: + + /** + * Creates a texture handle with known name and texture id. + * @param InName - Name of the texture + * @param InTextureId - ImGui id of texture + */ + FImGuiTextureHandle(const FName& InName, ImTextureID InTextureId); + + /** Checks if texture manager has entry that matches this name and texture id index. */ + bool HasValidEntry() const; + + FName Name; + ImTextureID TextureId; + + // Give module class a private access, so it can create valid handles. + friend class FImGuiModule; +};