imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Can ImDrawLists be reused in a later Render()?

Open AnimatorJeroen opened this issue 4 years ago • 13 comments

Version/Branch of Dear ImGui:

Version: 1.75

Back-ends: imgui_impl_glfw.cpp

Operating System: Windows

My Issue/Question: This may not be possible, and may not fit the immediate mode paradigm, but I'm still curious:

Can ImDrawLists be stored, and reused in a later Render() call?

XXX (please provide as much context as possible)

I want to do some cpu-intensive stuff that informs the drawing, that I wouldn't want to update every frame. I could create my own drawing command queue that only updates after user-input, and then add drawlist commands based on my queue on every ImGui call.

But then I thought, why the extra abstraction: the drawlist itself is a list of commands. Could a particular drawlist be stored and reused at a later time?

It tried the following, but it crashes:

static ImDrawListSharedData dummy_data;
static ImDrawList stored_draw_list = ImDrawList(&dummy_data);

	if (IsHovered || FirstTimeRendered)
	{
		//Do cpu intensive logic that informs drawing here
                    ImGui::GetCurrentWindow()->DrawList->AddLine(ImVec2(100*KA::Frame, 100), ImVec2(300, 1200), linescol, 5.0);
		stored_draw_list = ImDrawList(ImGui::GetCurrentWindow()->DrawList->_Data);
		FirstTimeRendered = false;
	}
	else
	{
		ImGui::GetCurrentWindow()->DrawList = &stored_draw_list;
	}

Would this at all be possible? Thanks in advance!

AnimatorJeroen avatar Oct 08 '20 23:10 AnimatorJeroen

Hello,

but it crashes:

As suggested in the guidelines, if you ever state "it crashes" please provide details and evidence in the form of a call-stack.

There's no copy constructor but you may currently use CloneOutput() to duplicate ImDrawList's output.

More specifically for what you seem to be trying to do (swap window's draw list) I believe we could do it more easily by flagging a window as Active but not clearing its ImDrawList contents, nor calling Begin(). I would like to use that technique to allow for windows with variable refresh rate, but it's not done yet.

ocornut avatar Oct 09 '20 08:10 ocornut

Sorry, there's the assertion: Assertion failed: _ClipRectStack.Size > 0 imgui_draw.cpp, line 516

stored_draw_list = *ImGui::GetCurrentWindow()->DrawList->CloneOutput(); Gives the same assertion.

It was a long shot anyway. The approach you describe sounds better. It sounds like, with that possibility, you could create a retained UI on top of the Immediate UI, to keep expensive calculations in the retained model and only use the immediate UI for drawing.

Here's a nice article about that approach, in case you are interested: https://www.gamasutra.com/blogs/NiklasGray/20170719/301963/One_Draw_Call_UI.php

Thanks for the quick response!

AnimatorJeroen avatar Oct 09 '20 09:10 AnimatorJeroen

to keep expensive calculations in the retained model and only use the immediate UI for drawing.

You are free to store expensive calculations however you want, I don't understand which problem you are trying to solve.

ocornut avatar Oct 09 '20 09:10 ocornut

It's just that now, I have the calculations of what to draw in a particular state, combined with the actual drawlist commands in my code. generic example:

if(ExpensiveCalculationA() && ExpensiveCalculationB() )
{
    x = ExpensiveCalculationC();
    y = ExpensiveCalculationD();
    size = ExpensiveCalculationE();
    draw_list->AddCircleFilled(ImVec2(x, y), size, col, 6);
}

I could separate the logic from the actual drawlist commands, storing x,y and size somewhere but I like the directness of ImGui to keep logic and draw commands together. By flagging a window as active but not clearing the drawlist as you say, that would be a very simple bloat-free approach to gain performance, whilst not complicating the code IMO. Then we would just redraw the previous state of a window, except when there's a user input that triggers the drawlist to be cleared and refilled. If that makes sense.

AnimatorJeroen avatar Oct 09 '20 10:10 AnimatorJeroen

For example, otherwise, I would need something like:

struct Circle
{
	float x, y
	float size, 
	ImU32 col
	void Draw()
	{
		draw_list->AddCircleFilled(ImVec2(x, y), size, col, 6);
	}
};
std::vector<Circle> circles;

If(UpdateWindow)
{
	circles.clear();
	if (ExpensiveCalculationA() && ExpensiveCalculationB())
	{
		x = ExpensiveCalculationC();
		y = ExpensiveCalculationD();
		size = ExpensiveCalculationE();
		circles.emplace_back(x,y,size, col);
	}
}
for (Circle c : circles)
	c.Draw();

AnimatorJeroen avatar Oct 09 '20 11:10 AnimatorJeroen

This related to #2391/#1878 right ?

To clarify the situation, correct me if I am wrong:

  • As explained by #2391, there is no such thing as ImGui::AddDrawList(&drawList)
  • Using GetBackgroundDrawList() still requires to copy vertices/indices data every frame
  • As stated in #1878 a way to save computation and copies, is by creating your own ImDrawData and reusing a ImDrawList without clearing its content to often and use a second ImGui_Impl*_RenderDrawData() call.

I am curious wether there are other methods ?

WildRackoon avatar Apr 26 '21 23:04 WildRackoon

Is there any update on this? @WildRackoon have you figured out anything?

I in particular would like to "cache" certain areas of a window. So, my idea is

  1. Call something like cacheBegin
  2. Create a new ImDrawList
  cacheData = new ImDrawListSharedData();
  cacheDrawList = new ImDrawList(cacheData);
  1. Replace current windows drawlist (store the original it somewhere to replace it back) to this one (so that any subsequent drawing happens to the cacheDrawList
  2. Call cacheEnd
  3. this should merge the cacheDrawList back to the originalDrawList

I was just trying to make this work, reached a point where it's not crashing, but what I'm drawing in the cache layer is not shown (and neither some draws after)

I roughly tried to follow ImDrawListSplitter::Merge, but have failed

Generally for the merging I'm doing

for (int i = 0; i < cacheDrawList->CmdBuffer.Size; i++) {
draw_list->CmdBuffer.push_back(cmd);
}
draw_list->PrimReserve(cacheDrawList->IdxBuffer.Size, cacheDrawList->VtxBuffer.Size);

for (int i = 0; i < cacheDrawList->VtxBuffer.Size; i++) {
  const ImDrawVert vert = cacheDrawList->VtxBuffer[i];
  draw_list->PrimWriteVtx(vert.pos, vert.uv, vert.col);
}
for (int i = 0; i < cacheDrawList->IdxBuffer.Size; i++) {
  draw_list->PrimWriteIdx(cacheDrawList->IdxBuffer[i]);
}

My guess is that it has something to do with the IdxOffset but I'm incredibly vague of vertex/textures etc.

Is what I'm trying to do possible? And if so,.. how?

As far as I understand what @ocornut has suggested is caching a whole window (by not clearing it's drawlist). I'm yet to try this actually, but another level of control (to be able to cache small areas of a window) would be useful I reckon.

Thanks in advance!

(edit: using PrimReserve & PrimWrite*)

actondev avatar Nov 05 '21 01:11 actondev

FYI, tangential: I have posted a ImDrawDataSnapshot class which allows efficiently taking a snapshot of a ImDrawData instance without full copy and with amortized allocations, you can find it there: (feedback welcome in that other thread) https://github.com/ocornut/imgui/issues/1860#issuecomment-1927630727 Also note that since 1.89.8 it became easier to add draw lists to an existing ImDrawData.

ocornut avatar Feb 05 '24 18:02 ocornut

I think it should be possible already.

When you want to update the ImDrawList:

  • Call _ResetForNewFrame(), followed (most likely) by PushClipRect(), PushTextureID().
  • Draw your stuff.
  • For convenience, you can add this draw list to an existing ImDrawData with ImDrawData::AddDrawList().

When you want to simply re-render:

  • Call ImDrawData::AddDrawList() only.

I think this should work. I realize this issue/request is old and it may not matter to do specially, but I'm happy to help further if needed. I also realize ImDrawData::AddDrawList() as a limitation that it always push_back to the end of ImDrawData, when in theory we could perfectly allow users to inject this ImDrawList anywhere in the list. For now it may be easy to call the function and reorder if desirable.

By flagging a window as active but not clearing the drawlist as you say, that would be a very simple bloat-free approach to gain performance, whilst not complicating the code IMO. Then we would just redraw the previous state of a window, except when there's a user input that triggers the drawlist to be cleared and refilled. If that makes sense.

That's planned and desirable but I think it may need to wait until we change the Begin() api (right now we allow drawing even when returning false). However it may be interesting for stuff like #7556 to experiment with the feature early on... even if it means using an internal API temporarily. I am going to toy with that.

ocornut avatar May 07 '24 08:05 ocornut

By flagging a window as active but not clearing the drawlist as you say, that would be a very simple bloat-free approach to gain performance, whilst not complicating the code IMO. Then we would just redraw the previous state of a window, except when there's a user input that triggers the drawlist to be cleared and refilled. If that makes sense.

That's planned and desirable but I think it may need to wait until we change the Begin() api (right now we allow drawing even when returning false).

I pushed an experiment: d449544 You can toy with it as ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags_TryToAvoidRefresh); (require imgui_internal.h) Note that this behave at the Window level, not on a per-DrawList basis. So it doesn't exactly full-fill the title of this topic, but it goes in the direction suggested by that paragraph of yours. See comments/caveats in the commit description. I think it'll be a good tool for reduce costs of "normally heavy" UI traversal, but may be an inadequate tool to reduce cost of "abnormally heavy" computation, as for that later you'll want very precise control and guaranteed avoidance that e.g. something is not done two frames in a row.

ocornut avatar May 07 '24 09:05 ocornut

Thank you! I will play with it when I have the time. Does the refreshPolicy only apply to the window's drawlist, or entire content, e.g. buttons as well?

AnimatorJeroen avatar May 07 '24 10:05 AnimatorJeroen

Currently the idea is that when not refreshed Begin() returns false and you can’t submit items anyhow. So everything will look frozen. There are flag to eg always refresh when hovered etc and the key will be to improve this design.

ocornut avatar May 07 '24 11:05 ocornut

Looking good, though what I'd ideally want for my use case are two more things (which can both be easily substituted but would make for a more complete API):

First is to be able to force an update for a specific window, since my code can notify the UI that it wants a UI update. I would like this to be a property of the window so the next update respects that and then clears it. Currently I could imagine a workaround that clears ImGuiNextWindowDataFlags_HasRefreshPolicy and then, after each Render(), sets it up again.

Second is to more easily know if the window UI was updated last frame - though this one is trivial to implement on my own since it's equivalent to checking if Begin returns true. I'd personally also set this flag to false myself since I already call RenderDrawData more than I call NewFrame (for GL views that need constant updates). This one is only really useful since I modified my OpenGL3 backend to be able to only update a small rectangle of the screen. This is because in my case, the rendering cost for the GPU is vastly outstripping the minimal CPU use of the UI code - so I'd actually just hijack this code to make GPU rendering less costly.

Thanks for the work on this front!

Seneral avatar Jun 12 '24 15:06 Seneral