From f57a73b91b10b2eaad37ec17e1ff62055787562b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 24 May 2018 21:55:25 +0100 Subject: [PATCH] Improved plugin to initialize correctly even when started during earlier loading phases: - Split tick and textures initializations. - Supporting deferred tick initialization, if Slate application is not ready during module startup. - Textures are lazy-loaded before the first use. - ImGui Context Manager initializes font atlas to make sure that ImGui resources are always initialized. --- Source/ImGui/Private/ImGuiContextManager.cpp | 4 + Source/ImGui/Private/ImGuiModuleManager.cpp | 125 +++++++++++-------- Source/ImGui/Private/ImGuiModuleManager.h | 12 +- 3 files changed, 83 insertions(+), 58 deletions(-) diff --git a/Source/ImGui/Private/ImGuiContextManager.cpp b/Source/ImGui/Private/ImGuiContextManager.cpp index 431d7f8..f76ccff 100644 --- a/Source/ImGui/Private/ImGuiContextManager.cpp +++ b/Source/ImGui/Private/ImGuiContextManager.cpp @@ -72,6 +72,10 @@ namespace FImGuiContextManager::FImGuiContextManager() { + unsigned char* Pixels; + int Width, Height, Bpp; + FontAtlas.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); + FWorldDelegates::OnWorldTickStart.AddRaw(this, &FImGuiContextManager::OnWorldTickStart); } diff --git a/Source/ImGui/Private/ImGuiModuleManager.cpp b/Source/ImGui/Private/ImGuiModuleManager.cpp index 6270bb7..e3038e7 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.cpp +++ b/Source/ImGui/Private/ImGuiModuleManager.cpp @@ -7,6 +7,8 @@ #include "ImGuiInteroperability.h" #include "Utilities/WorldContextIndex.h" +#include + #include @@ -15,15 +17,18 @@ FImGuiModuleManager::FImGuiModuleManager() // Typically we will use viewport created events to add widget to new game viewports. ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); - // Initialize resources and start ticking. Depending on loading phase, this may fail if Slate is not yet ready. - Initialize(); + // Try to register tick delegate (it may fail if Slate application isn't yet ready). + RegisterTick(); + + // If we failed to register, create an initializer that will do it later. + if (!IsTickRegistered()) + { + CreateTickInitializer(); + } // We need to add widgets to active game viewports as they won't generate on-created events. This is especially // important during hot-reloading. - if (bInitialized) - { - AddWidgetToAllViewports(); - } + AddWidgetsToActiveViewports(); } FImGuiModuleManager::~FImGuiModuleManager() @@ -45,56 +50,39 @@ FImGuiModuleManager::~FImGuiModuleManager() } // Deactivate this manager. - Uninitialize(); -} - -void FImGuiModuleManager::Initialize() -{ - // We rely on Slate, so we can only continue if it is already initialized. - if (!bInitialized && FSlateApplication::IsInitialized()) - { - bInitialized = true; - LoadTextures(); - RegisterTick(); - } -} - -void FImGuiModuleManager::Uninitialize() -{ - if (bInitialized) - { - bInitialized = false; - UnregisterTick(); - } + ReleaseTickInitializer(); + UnregisterTick(); } void FImGuiModuleManager::LoadTextures() { checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can create textures.")); - // 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); + if (!bTexturesLoaded) + { + bTexturesLoaded = true; - // Create a font atlas texture. - ImFontAtlas& Fonts = ContextManager.GetFontAtlas(); + // 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); - unsigned char* Pixels; - int Width, Height, Bpp; - Fonts.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); + // Create a font atlas texture. + ImFontAtlas& Fonts = ContextManager.GetFontAtlas(); - TextureIndex FontsTexureIndex = TextureManager.CreateTexture(FName{ "ImGuiModule_FontAtlas" }, Width, Height, Bpp, Pixels, false); + unsigned char* Pixels; + int Width, Height, Bpp; + Fonts.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp); - // Set font texture index in ImGui. - Fonts.TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex); + TextureIndex FontsTexureIndex = TextureManager.CreateTexture(FName{ "ImGuiModule_FontAtlas" }, Width, Height, Bpp, Pixels, false); + + // Set font texture index in ImGui. + Fonts.TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex); + } } void FImGuiModuleManager::RegisterTick() { - checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can register tick listener.")); - - // We will tick on Slate Post-Tick events. They are quite convenient as they happen at the very end of the frame, - // what helps to minimise tearing. - if (!TickDelegateHandle.IsValid()) + // Slate Post-Tick is a good moment to end and advance ImGui frame as it minimises a tearing. + if (!TickDelegateHandle.IsValid() && FSlateApplication::IsInitialized()) { TickDelegateHandle = FSlateApplication::Get().OnPostTick().AddRaw(this, &FImGuiModuleManager::Tick); } @@ -112,6 +100,36 @@ void FImGuiModuleManager::UnregisterTick() } } +void FImGuiModuleManager::CreateTickInitializer() +{ + if (!TickInitializerHandle.IsValid()) + { + // Try to register tick delegate until we finally succeed. + + TickInitializerHandle = FModuleManager::Get().OnModulesChanged().AddLambda([this](FName Name, EModuleChangeReason Reason) + { + if (Reason == EModuleChangeReason::ModuleLoaded) + { + RegisterTick(); + } + + if (IsTickRegistered()) + { + ReleaseTickInitializer(); + } + }); + } +} + +void FImGuiModuleManager::ReleaseTickInitializer() +{ + if (TickInitializerHandle.IsValid()) + { + FModuleManager::Get().OnModulesChanged().Remove(TickInitializerHandle); + TickInitializerHandle.Reset(); + } +} + bool FImGuiModuleManager::IsInUpdateThread() { // We can get ticks from the Game thread and Slate loading thread. In both cases IsInGameThread() is true, so we @@ -135,9 +153,6 @@ void FImGuiModuleManager::OnViewportCreated() { checkf(FSlateApplication::IsInitialized(), TEXT("We expect Slate to be initialized when game viewport is created.")); - // Make sure that all resources are initialized to handle configurations where this module is loaded before Slate. - Initialize(); - // Create widget to viewport responsible for this event. AddWidgetToViewport(GEngine->GameViewport); } @@ -147,13 +162,19 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) checkf(GameViewport, TEXT("Null game viewport.")); checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); - // This makes sure that context for this world is created. - auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld()); + // Make sure that we have a context for this viewport's world and get its index. + int32 ContextIndex; + auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld(), ContextIndex); + // Make sure that textures are loaded before the first Slate widget is created. + LoadTextures(); + + // Create and initialize the widget. TSharedPtr SharedWidget; - SAssignNew(SharedWidget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport) - .ContextIndex(Utilities::GetWorldContextIndex(GameViewport)); + SAssignNew(SharedWidget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport).ContextIndex(ContextIndex); + // We transfer widget ownerships to viewports but we keep weak references in case we need to manually detach active + // widgets during module shutdown (important during hot-reloading). if (TWeakPtr* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); })) { *Slot = SharedWidget; @@ -164,11 +185,9 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) } } -void FImGuiModuleManager::AddWidgetToAllViewports() +void FImGuiModuleManager::AddWidgetsToActiveViewports() { - checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); - - if (GEngine) + if (FSlateApplication::IsInitialized() && GEngine) { // Loop as long as we have a valid viewport or until we detect a cycle. UGameViewportClient* GameViewport = GEngine->GameViewport; diff --git a/Source/ImGui/Private/ImGuiModuleManager.h b/Source/ImGui/Private/ImGuiModuleManager.h index ee1b9ec..ff87972 100644 --- a/Source/ImGui/Private/ImGuiModuleManager.h +++ b/Source/ImGui/Private/ImGuiModuleManager.h @@ -35,14 +35,15 @@ private: FImGuiModuleManager(FImGuiModuleManager&&) = delete; FImGuiModuleManager& operator=(FImGuiModuleManager&&) = delete; - void Initialize(); - void Uninitialize(); - void LoadTextures(); + bool IsTickRegistered() { return TickDelegateHandle.IsValid(); } void RegisterTick(); void UnregisterTick(); + void CreateTickInitializer(); + void ReleaseTickInitializer(); + bool IsInUpdateThread(); void Tick(float DeltaSeconds); @@ -50,7 +51,7 @@ private: void OnViewportCreated(); void AddWidgetToViewport(UGameViewportClient* GameViewport); - void AddWidgetToAllViewports(); + void AddWidgetsToActiveViewports(); // Event that we call after ImGui is updated. FSimpleMulticastDelegate PostImGuiUpdateEvent; @@ -64,8 +65,9 @@ private: // Slate widgets that we created. TArray> Widgets; + FDelegateHandle TickInitializerHandle; FDelegateHandle TickDelegateHandle; FDelegateHandle ViewportCreatedHandle; - bool bInitialized = false; + bool bTexturesLoaded = false; };