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.
This commit is contained in:
Sebastian 2018-05-24 21:55:25 +01:00
parent ec5bc3e738
commit f57a73b91b
3 changed files with 83 additions and 58 deletions

View File

@ -72,6 +72,10 @@ namespace
FImGuiContextManager::FImGuiContextManager() FImGuiContextManager::FImGuiContextManager()
{ {
unsigned char* Pixels;
int Width, Height, Bpp;
FontAtlas.GetTexDataAsRGBA32(&Pixels, &Width, &Height, &Bpp);
FWorldDelegates::OnWorldTickStart.AddRaw(this, &FImGuiContextManager::OnWorldTickStart); FWorldDelegates::OnWorldTickStart.AddRaw(this, &FImGuiContextManager::OnWorldTickStart);
} }

View File

@ -7,6 +7,8 @@
#include "ImGuiInteroperability.h" #include "ImGuiInteroperability.h"
#include "Utilities/WorldContextIndex.h" #include "Utilities/WorldContextIndex.h"
#include <ModuleManager.h>
#include <imgui.h> #include <imgui.h>
@ -15,15 +17,18 @@ FImGuiModuleManager::FImGuiModuleManager()
// Typically we will use viewport created events to add widget to new game viewports. // Typically we will use viewport created events to add widget to new game viewports.
ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); 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. // Try to register tick delegate (it may fail if Slate application isn't yet ready).
Initialize(); 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 // We need to add widgets to active game viewports as they won't generate on-created events. This is especially
// important during hot-reloading. // important during hot-reloading.
if (bInitialized) AddWidgetsToActiveViewports();
{
AddWidgetToAllViewports();
}
} }
FImGuiModuleManager::~FImGuiModuleManager() FImGuiModuleManager::~FImGuiModuleManager()
@ -45,33 +50,18 @@ FImGuiModuleManager::~FImGuiModuleManager()
} }
// Deactivate this manager. // Deactivate this manager.
Uninitialize(); ReleaseTickInitializer();
}
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(); UnregisterTick();
} }
}
void FImGuiModuleManager::LoadTextures() void FImGuiModuleManager::LoadTextures()
{ {
checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can create textures.")); checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can create textures."));
if (!bTexturesLoaded)
{
bTexturesLoaded = true;
// Create an empty texture at index 0. We will use it for ImGui outputs with null texture id. // 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); TextureManager.CreatePlainTexture(FName{ "ImGuiModule_Plain" }, 2, 2, FColor::White);
@ -87,14 +77,12 @@ void FImGuiModuleManager::LoadTextures()
// Set font texture index in ImGui. // Set font texture index in ImGui.
Fonts.TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex); Fonts.TexID = ImGuiInterops::ToImTextureID(FontsTexureIndex);
} }
}
void FImGuiModuleManager::RegisterTick() void FImGuiModuleManager::RegisterTick()
{ {
checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can register tick listener.")); // Slate Post-Tick is a good moment to end and advance ImGui frame as it minimises a tearing.
if (!TickDelegateHandle.IsValid() && FSlateApplication::IsInitialized())
// 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())
{ {
TickDelegateHandle = FSlateApplication::Get().OnPostTick().AddRaw(this, &FImGuiModuleManager::Tick); 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() bool FImGuiModuleManager::IsInUpdateThread()
{ {
// We can get ticks from the Game thread and Slate loading thread. In both cases IsInGameThread() is true, so we // 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.")); 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. // Create widget to viewport responsible for this event.
AddWidgetToViewport(GEngine->GameViewport); AddWidgetToViewport(GEngine->GameViewport);
} }
@ -147,13 +162,19 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport)
checkf(GameViewport, TEXT("Null game viewport.")); checkf(GameViewport, TEXT("Null game viewport."));
checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); 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. // Make sure that we have a context for this viewport's world and get its index.
auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld()); 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<SImGuiWidget> SharedWidget; TSharedPtr<SImGuiWidget> SharedWidget;
SAssignNew(SharedWidget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport) SAssignNew(SharedWidget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport).ContextIndex(ContextIndex);
.ContextIndex(Utilities::GetWorldContextIndex(GameViewport));
// 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<SImGuiWidget>* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); })) if (TWeakPtr<SImGuiWidget>* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); }))
{ {
*Slot = SharedWidget; *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 (FSlateApplication::IsInitialized() && GEngine)
if (GEngine)
{ {
// Loop as long as we have a valid viewport or until we detect a cycle. // Loop as long as we have a valid viewport or until we detect a cycle.
UGameViewportClient* GameViewport = GEngine->GameViewport; UGameViewportClient* GameViewport = GEngine->GameViewport;

View File

@ -35,14 +35,15 @@ private:
FImGuiModuleManager(FImGuiModuleManager&&) = delete; FImGuiModuleManager(FImGuiModuleManager&&) = delete;
FImGuiModuleManager& operator=(FImGuiModuleManager&&) = delete; FImGuiModuleManager& operator=(FImGuiModuleManager&&) = delete;
void Initialize();
void Uninitialize();
void LoadTextures(); void LoadTextures();
bool IsTickRegistered() { return TickDelegateHandle.IsValid(); }
void RegisterTick(); void RegisterTick();
void UnregisterTick(); void UnregisterTick();
void CreateTickInitializer();
void ReleaseTickInitializer();
bool IsInUpdateThread(); bool IsInUpdateThread();
void Tick(float DeltaSeconds); void Tick(float DeltaSeconds);
@ -50,7 +51,7 @@ private:
void OnViewportCreated(); void OnViewportCreated();
void AddWidgetToViewport(UGameViewportClient* GameViewport); void AddWidgetToViewport(UGameViewportClient* GameViewport);
void AddWidgetToAllViewports(); void AddWidgetsToActiveViewports();
// Event that we call after ImGui is updated. // Event that we call after ImGui is updated.
FSimpleMulticastDelegate PostImGuiUpdateEvent; FSimpleMulticastDelegate PostImGuiUpdateEvent;
@ -64,8 +65,9 @@ private:
// Slate widgets that we created. // Slate widgets that we created.
TArray<TWeakPtr<SImGuiWidget>> Widgets; TArray<TWeakPtr<SImGuiWidget>> Widgets;
FDelegateHandle TickInitializerHandle;
FDelegateHandle TickDelegateHandle; FDelegateHandle TickDelegateHandle;
FDelegateHandle ViewportCreatedHandle; FDelegateHandle ViewportCreatedHandle;
bool bInitialized = false; bool bTexturesLoaded = false;
}; };