Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can ImDrawLists be reused in a later Render()? #3515

Open
AnimatorJeroen opened this issue Oct 8, 2020 · 13 comments
Open

Can ImDrawLists be reused in a later Render()? #3515

AnimatorJeroen opened this issue Oct 8, 2020 · 13 comments

Comments

@AnimatorJeroen
Copy link

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!

@ocornut
Copy link
Owner

ocornut commented Oct 9, 2020

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.

@AnimatorJeroen
Copy link
Author

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!

@ocornut
Copy link
Owner

ocornut commented Oct 9, 2020

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.

@AnimatorJeroen
Copy link
Author

AnimatorJeroen commented Oct 9, 2020

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
Copy link
Author

AnimatorJeroen commented Oct 9, 2020

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();

@WildRackoon
Copy link

WildRackoon commented Apr 26, 2021

This related to #2391/#1878 right ?

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

  • As explained by Draw graphics in the background #2391, there is no such thing as ImGui::AddDrawList(&drawList)
  • Using GetBackgroundDrawList() still requires to copy vertices/indices data every frame
  • As stated in Standalone ImDrawList use and rendering #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 ?

@actondev
Copy link

actondev commented Nov 5, 2021

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*)

ocornut added a commit that referenced this issue Jul 12, 2023
…DrawData itself. Faclitate user-manipulation of the array (#6406, #4879, #1878) + deep swap. (#6597, #6475, #6167, #5776, #5109, #4763, #3515, #1860)

+ Metrics: avoid misleadingly iterating all layers of DrawDataBuilder as everything is flattened into Layers[0] at this point.

# Conflicts:
#	imgui.cpp
#	imgui_internal.h
ocornut added a commit that referenced this issue Aug 6, 2023
@ocornut
Copy link
Owner

ocornut commented Feb 5, 2024

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)
#1860 (comment)
Also note that since 1.89.8 it became easier to add draw lists to an existing ImDrawData.

@ocornut
Copy link
Owner

ocornut commented May 7, 2024

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 added a commit that referenced this issue May 7, 2024
…#7556, #5116 , #4076, #2749, #2268)

currently: ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags_TryToAvoidRefresh);
- This is NOT meant to replace frame-wide/app-wide idle mode.
- This is another tool: the idea that a given window could avoid refresh and reuse last frame contents.
- I think it needs to be backed by a careful and smart design overall (refresh policy, load balancing, making it easy and obvious to user).
- It's not there yet, this is currently a toy for experimenting.
My other issues with this:
- It appears to be very simple, but skipping most of Begin() logic will inevitably lead to tricky/confusing bugs. Let's see how it goes.
- I don't like very much that this opens a door to varying inconsistencies
- I don't like very much that it can lead us to situation where the lazy refresh gets disabled in bulk due to some reason (e.g. resizing a dock space) and we get sucked in the temptation to update for idle rather than update for dynamism.
@ocornut
Copy link
Owner

ocornut commented May 7, 2024

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.

@AnimatorJeroen
Copy link
Author

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?

@ocornut
Copy link
Owner

ocornut commented May 7, 2024

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.

@Seneral
Copy link

Seneral commented Jun 12, 2024

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants