diff --git a/Source/ImGui/Private/ImGuiContextManager.cpp b/Source/ImGui/Private/ImGuiContextManager.cpp index 570c29b..21df97f 100644 --- a/Source/ImGui/Private/ImGuiContextManager.cpp +++ b/Source/ImGui/Private/ImGuiContextManager.cpp @@ -12,11 +12,6 @@ #include -// Index of the currently updated context. Only valid during context manager tick. -// TODO: Move to public interface (but probably as a current world/viewport etc.) -int32 CurrentContextIndex = Utilities::INVALID_CONTEXT_INDEX; - - namespace { #if WITH_EDITOR @@ -85,13 +80,10 @@ void FImGuiContextManager::Tick(float DeltaSeconds) for (auto& Pair : Contexts) { - auto ContextIndexSave = ScopeGuards::MakeStateSaver(CurrentContextIndex); - CurrentContextIndex = Pair.Key; auto& ContextData = Pair.Value; if (ContextData.CanTick()) { - ContextData.ContextProxy.SetAsCurrent(); - ContextData.ContextProxy.Tick(DeltaSeconds, &DrawMultiContextEvent); + ContextData.ContextProxy.Tick(DeltaSeconds); } } } @@ -113,7 +105,7 @@ FImGuiContextManager::FContextData& FImGuiContextManager::GetEditorContextData() if (UNLIKELY(!Data)) { - Data = &Contexts.Emplace(Utilities::EDITOR_CONTEXT_INDEX, FContextData{ GetEditorContextName(), ImGuiDemo }); + Data = &Contexts.Emplace(Utilities::EDITOR_CONTEXT_INDEX, FContextData{ GetEditorContextName(), Utilities::EDITOR_CONTEXT_INDEX, DrawMultiContextEvent, ImGuiDemo, -1 }); } return *Data; @@ -127,7 +119,7 @@ FImGuiContextManager::FContextData& FImGuiContextManager::GetStandaloneWorldCont if (UNLIKELY(!Data)) { - Data = &Contexts.Emplace(Utilities::STANDALONE_GAME_CONTEXT_INDEX, FContextData{ GetWorldContextName(), ImGuiDemo }); + Data = &Contexts.Emplace(Utilities::STANDALONE_GAME_CONTEXT_INDEX, FContextData{ GetWorldContextName(), Utilities::STANDALONE_GAME_CONTEXT_INDEX, DrawMultiContextEvent, ImGuiDemo }); } return *Data; @@ -167,7 +159,7 @@ FImGuiContextManager::FContextData& FImGuiContextManager::GetWorldContextData(co #if WITH_EDITOR if (UNLIKELY(!Data)) { - Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), ImGuiDemo, WorldContext->PIEInstance }); + Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), Index, DrawMultiContextEvent, ImGuiDemo, WorldContext->PIEInstance }); } else { @@ -177,7 +169,7 @@ FImGuiContextManager::FContextData& FImGuiContextManager::GetWorldContextData(co #else if (UNLIKELY(!Data)) { - Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), ImGuiDemo }); + Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), Index, DrawMultiContextEvent, ImGuiDemo }); } #endif diff --git a/Source/ImGui/Private/ImGuiContextManager.h b/Source/ImGui/Private/ImGuiContextManager.h index ea94e40..6664eee 100644 --- a/Source/ImGui/Private/ImGuiContextManager.h +++ b/Source/ImGui/Private/ImGuiContextManager.h @@ -56,11 +56,11 @@ private: struct FContextData { - FContextData(const FString& ContextName, FImGuiDemo& Demo, int32 InPIEInstance = -1) + FContextData(const FString& ContextName, int32 ContextIndex, FSimpleMulticastDelegate& SharedDrawEvent, FImGuiDemo& Demo, int32 InPIEInstance = -1) : PIEInstance(InPIEInstance) - , ContextProxy(ContextName) + , ContextProxy(ContextName, &SharedDrawEvent) { - ContextProxy.OnDraw().AddRaw(&Demo, &FImGuiDemo::DrawControls); + ContextProxy.OnDraw().AddLambda([&Demo, ContextIndex]() { Demo.DrawControls(ContextIndex); }); } FORCEINLINE bool CanTick() const { return PIEInstance < 0 || GEngine->GetWorldContextFromPIEInstance(PIEInstance); } @@ -73,10 +73,10 @@ private: struct FContextData { - FContextData(const FString& ContextName, FImGuiDemo& Demo) - : ContextProxy(ContextName) + FContextData(const FString& ContextName, int32 ContextIndex, FSimpleMulticastDelegate& SharedDrawEvent, FImGuiDemo& Demo) + : ContextProxy(ContextName, &SharedDrawEvent) { - ContextProxy.OnDraw().AddRaw(&Demo, &FImGuiDemo::DrawControls); + ContextProxy.OnDraw().AddLambda([&Demo, ContextIndex]() { Demo.DrawControls(ContextIndex); }); } FORCEINLINE bool CanTick() const { return true; } diff --git a/Source/ImGui/Private/ImGuiContextProxy.cpp b/Source/ImGui/Private/ImGuiContextProxy.cpp index 61281da..8804860 100644 --- a/Source/ImGui/Private/ImGuiContextProxy.cpp +++ b/Source/ImGui/Private/ImGuiContextProxy.cpp @@ -39,12 +39,13 @@ namespace } } -FImGuiContextProxy::FImGuiContextProxy(const FString& InName) +FImGuiContextProxy::FImGuiContextProxy(const FString& InName, FSimpleMulticastDelegate* InSharedDrawEvent) : Name(InName) + , SharedDrawEvent(InSharedDrawEvent) , IniFilename(TCHAR_TO_ANSI(*GetIniFile(InName))) { // Create context. - Context = ImGui::CreateContext(); + Context = TUniquePtr(ImGui::CreateContext()); // Set this context in ImGui for initialization (any allocations will be tracked in this context). SetAsCurrent(); @@ -80,35 +81,6 @@ FImGuiContextProxy::FImGuiContextProxy(const FString& InName) BeginFrame(); } -FImGuiContextProxy::FImGuiContextProxy(FImGuiContextProxy&& Other) - : Context(std::move(Other.Context)) - , bHasActiveItem(Other.bHasActiveItem) - , DrawEvent(std::move(Other.DrawEvent)) - , InputState(std::move(Other.InputState)) - , DrawLists(std::move(Other.DrawLists)) - , Name(std::move(Other.Name)) - , IniFilename(std::move(Other.IniFilename)) -{ - Other.Context = nullptr; -} - -FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other) -{ - // Swapping context so it can be destroyed with the other object. - using std::swap; - swap(Context, Other.Context); - - // Just moving remaining data that doesn't affect cleanup. - bHasActiveItem = Other.bHasActiveItem; - DrawEvent = std::move(Other.DrawEvent); - InputState = std::move(Other.InputState); - DrawLists = std::move(Other.DrawLists); - Name = std::move(Other.Name); - IniFilename = std::move(Other.IniFilename); - - return *this; -} - FImGuiContextProxy::~FImGuiContextProxy() { if (Context) @@ -118,39 +90,64 @@ FImGuiContextProxy::~FImGuiContextProxy() // Save context data and destroy. ImGuiImplementation::SaveCurrentContextIniSettings(IniFilename.c_str()); - ImGui::DestroyContext(Context); + ImGui::DestroyContext(Context.Release()); // Set default context in ImGui to keep global context pointer valid. ImGui::SetCurrentContext(&ImGuiImplementation::GetDefaultContext()); } } -void FImGuiContextProxy::Tick(float DeltaSeconds, FSimpleMulticastDelegate* SharedDrawEvent) +void FImGuiContextProxy::Draw() { - if (bIsFrameStarted) + // Comparing to LastTickFrameNumber rather than GFrameNumber to make sure that this is driven by our own update cycle. + if (LastDrawFrameNumber < LastTickFrameNumber) { - // Broadcast draw event to allow listeners to draw their controls to this context. - if (DrawEvent.IsBound()) - { - DrawEvent.Broadcast(); - } - if (SharedDrawEvent && SharedDrawEvent->IsBound()) - { - SharedDrawEvent->Broadcast(); - } + LastDrawFrameNumber = LastTickFrameNumber; - // Ending frame will produce render output that we capture and store for later use. This also puts context to - // state in which it does not allow to draw controls, so we want to immediately start a new frame. - EndFrame(); + if (bIsFrameStarted) + { + SetAsCurrent(); + + // Broadcast draw event to allow listeners to draw their controls to this context. + if (DrawEvent.IsBound()) + { + DrawEvent.Broadcast(); + } + if (SharedDrawEvent && SharedDrawEvent->IsBound()) + { + SharedDrawEvent->Broadcast(); + } + } } +} - // Update context information (some data, like mouse cursor, may be cleaned in new frame, so we should collect it - // beforehand). - bHasActiveItem = ImGui::IsAnyItemActive(); - MouseCursor = ImGuiInterops::ToSlateMouseCursor(ImGui::GetMouseCursor()); +void FImGuiContextProxy::Tick(float DeltaSeconds) +{ + // Making sure that we tick only once per frame. + if (LastTickFrameNumber < GFrameNumber) + { + LastTickFrameNumber = GFrameNumber; - // Begin a new frame and set the context back to a state in which it allows to draw controls. - BeginFrame(DeltaSeconds); + SetAsCurrent(); + + if (bIsFrameStarted) + { + // Make sure that draw events are called before the end of the frame. + Draw(); + + // Ending frame will produce render output that we capture and store for later use. This also puts context to + // state in which it does not allow to draw controls, so we want to immediately start a new frame. + EndFrame(); + } + + // Update context information (some data, like mouse cursor, may be cleaned in new frame, so we should collect it + // beforehand). + bHasActiveItem = ImGui::IsAnyItemActive(); + MouseCursor = ImGuiInterops::ToSlateMouseCursor(ImGui::GetMouseCursor()); + + // Begin a new frame and set the context back to a state in which it allows to draw controls. + BeginFrame(DeltaSeconds); + } } void FImGuiContextProxy::BeginFrame(float DeltaTime) diff --git a/Source/ImGui/Private/ImGuiContextProxy.h b/Source/ImGui/Private/ImGuiContextProxy.h index 9bb80c9..817f9ea 100644 --- a/Source/ImGui/Private/ImGuiContextProxy.h +++ b/Source/ImGui/Private/ImGuiContextProxy.h @@ -19,14 +19,14 @@ class FImGuiContextProxy { public: - FImGuiContextProxy(const FString& Name); + FImGuiContextProxy(const FString& Name, FSimpleMulticastDelegate* InSharedDrawEvent); ~FImGuiContextProxy(); FImGuiContextProxy(const FImGuiContextProxy&) = delete; FImGuiContextProxy& operator=(const FImGuiContextProxy&) = delete; - FImGuiContextProxy(FImGuiContextProxy&& Other); - FImGuiContextProxy& operator=(FImGuiContextProxy&& Other); + FImGuiContextProxy(FImGuiContextProxy&& Other) = default; + FImGuiContextProxy& operator=(FImGuiContextProxy&& Other) = default; // Get the name of this context. const FString& GetName() const { return Name; } @@ -44,10 +44,10 @@ public: void RemoveInputState(const FImGuiInputState* InputStateToRemove) { if (InputState == InputStateToRemove) InputState = nullptr; } // Is this context the current ImGui context. - bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; } + bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context.Get(); } // Set this context as current ImGui context. - void SetAsCurrent() { ImGui::SetCurrentContext(Context); } + void SetAsCurrent() { ImGui::SetCurrentContext(Context.Get()); } bool HasActiveItem() const { return bHasActiveItem; } @@ -56,9 +56,12 @@ public: // Delegate called right before ending the frame to allows listeners draw their controls. FSimpleMulticastDelegate& OnDraw() { return DrawEvent; } - // Tick to advance context to the next frame. - // @param SharedDrawEvent - Shared draw event provided from outside to be called right after context own event - void Tick(float DeltaSeconds, FSimpleMulticastDelegate* SharedDrawEvent = nullptr); + // Call draw events to allow listeners draw their widgets. Only one call per frame is processed. If it is not + // called manually before, then it will be called from the Tick function. + void Draw(); + + // Tick to advance context to the next frame. Only one call per frame will be processed. + void Tick(float DeltaSeconds); private: @@ -67,13 +70,18 @@ private: void UpdateDrawData(ImDrawData* DrawData); - ImGuiContext* Context = nullptr; + TUniquePtr Context; - bool bHasActiveItem = false; EMouseCursor::Type MouseCursor = EMouseCursor::None; + bool bHasActiveItem = false; bool bIsFrameStarted = false; FSimpleMulticastDelegate DrawEvent; + FSimpleMulticastDelegate* SharedDrawEvent = nullptr; + + uint32 LastTickFrameNumber = 0; + uint32 LastDrawFrameNumber = 0; + const FImGuiInputState* InputState = nullptr; TArray DrawLists; diff --git a/Source/ImGui/Private/ImGuiDemo.cpp b/Source/ImGui/Private/ImGuiDemo.cpp index 02a93eb..c171738 100644 --- a/Source/ImGui/Private/ImGuiDemo.cpp +++ b/Source/ImGui/Private/ImGuiDemo.cpp @@ -17,13 +17,11 @@ namespace CVars } // Demo copied (with minor modifications) from ImGui examples. See https://github.com/ocornut/imgui. -void FImGuiDemo::DrawControls() +void FImGuiDemo::DrawControls(int32 ContextIndex) { if (CVars::ShowDemo.GetValueOnGameThread() > 0) { - // TODO: This should be part of a public interface. - extern int32 CurrentContextIndex; - const int32 ContextBit = CurrentContextIndex < 0 ? 0 : 1 << CurrentContextIndex; + const int32 ContextBit = ContextIndex < 0 ? 0 : 1 << ContextIndex; // 1. Show a simple window // Tip: if we don't call ImGui::Begin()/ImGui::End() the widgets appears in a window automatically called "Debug" @@ -53,13 +51,28 @@ void FImGuiDemo::DrawControls() // 3. Show the ImGui test window. Most of the sample code is in ImGui::ShowTestWindow() if (ShowDemoWindowMask & ContextBit) { - // Display warning about running ImGui examples in multiple contexts. - if (ShowDemoWindowMask != ContextBit) + // If more than one demo window is opened display warning about running ImGui examples in multiple contexts. + + // For everything, but the first windows in this frame we assume warning. + bool bWarning = true; + if (GFrameNumber > LastDemoWindowFrameNumber) + { + // If this is the first window in this frame, then we need to look at the last frame to see whether + // there were more than one windows. Higher frame distance automatically means that there were not. + bWarning = ((GFrameNumber - LastDemoWindowFrameNumber) == 1) && (DemoWindowCounter > 1); + + LastDemoWindowFrameNumber = GFrameNumber; + DemoWindowCounter = 0; + } + + DemoWindowCounter++; + + if (bWarning) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, { 1.f, 1.f, 0.5f, 1.f }); - ImGui::TextWrapped("Demo Window is opend in more than one context, some of the ImGui examples may not work correctly."); + ImGui::TextWrapped("Demo Window is opened in more than one context, some of the ImGui examples may not work correctly."); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) @@ -70,6 +83,8 @@ void FImGuiDemo::DrawControls() "If you have a problem with an example try to run it in one context only."); } } + + // Draw demo window. ImGui::SetNextWindowPos(ImVec2(650, 20), ImGuiSetCond_FirstUseEver); ImGui::ShowDemoWindow(); } diff --git a/Source/ImGui/Private/ImGuiDemo.h b/Source/ImGui/Private/ImGuiDemo.h index f620990..6534fb3 100644 --- a/Source/ImGui/Private/ImGuiDemo.h +++ b/Source/ImGui/Private/ImGuiDemo.h @@ -10,7 +10,7 @@ class FImGuiDemo { public: - void DrawControls(); + void DrawControls(int32 ContextIndex); private: @@ -18,4 +18,7 @@ private: int32 ShowDemoWindowMask = 0; int32 ShowAnotherWindowMask = 0; + + int32 DemoWindowCounter = 0; + uint32 LastDemoWindowFrameNumber = 0; }; diff --git a/Source/ImGui/Private/SImGuiWidget.cpp b/Source/ImGui/Private/SImGuiWidget.cpp index b689b10..8148b11 100644 --- a/Source/ImGui/Private/SImGuiWidget.cpp +++ b/Source/ImGui/Private/SImGuiWidget.cpp @@ -433,6 +433,10 @@ int32 SImGuiWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeo { if (FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) { + // Manually update ImGui context to minimise lag between creating and rendering ImGui output. This will also + // keep frame tearing at minimum because it is executed at the very end of the frame. + ContextProxy->Tick(FSlateApplication::Get().GetDeltaTime()); + // Calculate offset that will transform vertex positions to screen space - rounded to avoid half pixel offsets. const FVector2D VertexPositionOffset{ FMath::RoundToFloat(MyClippingRect.Left), FMath::RoundToFloat(MyClippingRect.Top) };