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

Added power saving mode (docking branch) #4076

Open
wants to merge 2 commits into
base: docking
Choose a base branch
from

Conversation

corentin-plouet
Copy link

@corentin-plouet corentin-plouet commented Apr 26, 2021

This is the docking/viewport branch version of the power saving mode implemented in #2749. That PR has a lot of background regarding how that feature came to be.

The next steps of the plan are:

  • Make sure I did not screw up the cherry-pick
  • Merge in the enhancements made by @bvgastel
  • See how we can make this work properly with multiple viewports (@ocornut's comment provides good guidance; i.e. we should try to make cursor blinking and animation per viewport, but otherwise make any user input refresh all of them).

@corentin-plouet
Copy link
Author

@ocornut / @rokups could we please have the CI enabled for this PR if possible? It's touching examples across multiple platforms, so would be very helpful. Thanks!

@corentin-plouet corentin-plouet changed the title Added power save mode Added power saving mode (docking branch) Apr 26, 2021
@ocornut
Copy link
Owner

ocornut commented Apr 26, 2021

Done. (I didn’t realize workflows now needed approval!)

@PathogenDavid
Copy link
Contributor

@jlmxyz
Copy link

jlmxyz commented Aug 27, 2021

any improvement regarding the cpu usage? really, it's ages that it's working, should be merged... without cpu usage reduction what the point of having a blast fast GUI when even a calc app consume 30% cpu???? how can you imagine having 30+ imgui based app running each consuming 30% CPU?

@rokups
Copy link
Contributor

rokups commented Aug 30, 2021

Typical Dear ImGui application consumes next to no CPU. If your application consumes 30% of CPU then i suggest looking for possible inefficiencies in your code. Here is what CPU consumption of sample application looks like for me:
image

@bvgastel
Copy link

That is not in line with my own observations. For the demo window, I typically get on Linux without this MR:

  • 8-9% CPU usage glfw + opengl3 example;
  • ~6% CPU usage sdl + opengl3 example.

Drops to 0% with my energy branch.

@rokups Some examples/backends/platforms support a check if a window is visible, and if the window is not visible don't draw. Is your window visible when you look at the CPU usage?

@rokups
Copy link
Contributor

rokups commented Aug 31, 2021

It is visible. That reeding depends on some settings. When value is "scaled to 100%" it shows 0.0%, when scaling is disabled it shows 2%. Probably also depends on CPU..

@ocornut
Copy link
Owner

ocornut commented Aug 31, 2021 via email

@jlmxyz
Copy link

jlmxyz commented Sep 8, 2021 via email

@pthom
Copy link
Contributor

pthom commented Mar 25, 2022

I tried to cherry-pick again this PR onto the latest docking branch.
It was successful, but I had to re-apply manually the changes in imgui_impl_glfw (since this backend had been reviewed extensively).

You can see the results in the commits here (this is a branch that cherry picks and adapts your changes)

Summary:

  • f558d37 is a cherry pick of your changes (but without the glfw changes)
  • ea416c8 reapplies the changes for the glfw backend
  • Also, I had to add SDLWindow *, GLFWwindow *, HWND params to the various ImGui_ImplXXXX_WaitForEvent functions (see 338fdf3 and ce41ede)

Additional notes:

  • I also added a modification in order to handle buttons with the repeat flag
    (buttons that trigger continuously as long as the mouse is down)
    See commit 986982a , which is probably still a draft).

Benchmarks:

On my Mac (MacBook Pro Intel 2019), I have the following results, when using the backends SDL+OpenGL3 or GLFW+OpenGL3

As you see, without the powersave changes the GPU jumps to 24% (and this causes my mac to become very hot, very quickly)

Mode CPU Usage GPU Usage
powersave / idle 0% 0%
powersave / interacting 7.8% 8%
standard / idle 5.1% 24% (!)
standard / interacting 9.5 19%

Copy link

@Tetrapak0 Tetrapak0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use the SDL backend. I have noticed that no frames are rendered whatsoever if no events occur, which is unintuitive, if the end user is waiting for a job to finish and then display a result or something and they are not moving the mouse. This could be circumvented by only using waitforevent if a condition is met. let's say a should_wait variable, but a better way would be to still render a few frames every second. Let's say 2. That way there is no need for the ImGui::GetEventWaitingTime() function you have added, or it should at least be modified, so that infinity could not be returned. Another way would be to just specify waiting time to 0.5s if window isn't hidden, and remove waiting_time_ms; change the value in the function call to 500.

@pthom
Copy link
Contributor

pthom commented Oct 19, 2023

Iet's say a should_wait variable, but a better way would be to still render a few frames every second. Let's say 2.

For an alternative approach, you could also look at https://github.com/ocornut/imgui/wiki/Implementing-Power-Save,-aka-Idling-outside-of-ImGui

@Seneral
Copy link

Seneral commented Apr 3, 2024

I've implemented something to reduce GPU usage for my desktop application, which may have GL views embedded via a render list callback (AddCallback) and some other text controls which may want to be updated at high frequency.
My test has been run on a 7840HS laptop with 165Hz, 2560x1600 screen, in three stages of adaption.
I've chosen to use power usage measured with turbostat (pkgWatt) instead of GPU usage directly, since GPU usage via radeontop was more complex to analyse.

Before - Free-running at screen refresh rate of 165Hz:

Idling: 12W - GPU usage: 70% clip, 50% fragment
Fast Update Mode: 12W - GPU usage: 70% clip, 50% fragment

Before - Free-running at screen refresh rate of 60Hz:

Idling: 6.5W - GPU usage: 30% clip, 25% fragment
Fast Update Mode: 6.5W - GPU usage: 30% clip, 25% fragment

Fast Update Mode is a game-like mode where at least some parts of my app need 60Hz updates.

For both of my adaptations, I chose to lock the maximum update rate to 60Hz in the app as well as with the screen.

With simple event updates and additional full-screen rendering in Fast Update Mode:

Idling: 4.8W - GPU Usage 0%
Light Usage: ~7.0W - GPU Usage: 40% clip, 30% fragment
Fast Update Mode: 6.8W - GPU Usage: 27% clip, 22% fragment
Note that here, in fast update mode, since it's only rendering at a higher update rate, some UI labels don't update until you trigger an input event

With simple event updates and partial rendering and on-demand UI rendering:

Idling: 4.8W - GPU Usage 0%
Light Usage: ~7.0W - GPU Usage: 40% clip, 30% fragment
Fast Update Mode:
5.3W (Rendering just label, 0.1% of the screen) - GPU Usage: 12% clip, 3-4% fragment
5.8W (Rendering label + GL views, 38% of the screen) - GPU Usage: 15% clip, 8% fragment

The last adaptation uses a modified rendering backend implementation that allows me to draw only parts of the screen.
Here I use callbacks to render what I need with UI still properly integrated - e.g. my GL views, or even a UI label that I render on-demand in the callback.

Just for fun, the same last adaptation but at 165Hz screen, with fast update mode rendering at ~120Hz:

Idling: 4.8W - GPU Usage 0%
Light Usage: ~12W - GPU Usage: 70% clip, 50% fragment
Fast Update Mode @ 120Hz:
7.5W (Rendering just label, 0.1% of the screen) - GPU Usage: 30% clip, 8% fragment
8.0W (Rendering label + GL views, 38% of the screen) - GPU Usage: 35% clip, 17% fragment

Note that the GL views in this tests were trivially simple, so only the fillrate should have affected the results.
Reading the GPU usage values was often quite difficult, so assume a +- 10 (in absolute % usage) for each reading.

In conclusion:

  1. Event based rendering does a good job of keeping idle power draw at minimum.
    But when interacting with system, it's still always rendering the whole screen, so some light mouse movements let it spike to the maximum power draw (which depends on the screen refresh rate).
  2. This also does not address the use case when fast visual updates are needed by the application. There, you can avoid an ImGUI New Frame, but that by itself doesn't do much.
  3. Instead, after a quick modification of the rendering backend, you can update only the part of the screen with e.g. your GL view (and still render the UI ontop properly), which yields a good amount of power saving.
  4. Now if the parts of the screen that need updates include text and labels, which would typically require a ImGui update, my custom on-demand text rendering can help, too, and it's super fast to update just that.

I have not tested yet if doing a ImGui New Frame + partial render of areas that changed is a good compromise, but maybe it will be, since the NewFrame (e.g., purely CPU work) did not affect the power draw much for me, the GPU dominated the power draw.
I also did not optimise the uploading of vertices to the GPU, as that would be a bit more involved, but I assume the Fast Update Mode values would look a bit better since I am calling the rendering function twice (once for all 3 GL views, once for the label), and it's uploading all the data in each of them, even if it is clipped on the CPU (unless the amd driver skips the upload when it's unused).

While the power draw savings seen here don't seem like much, they are the difference between the laptop staying quiet and the fans being on full blast for me.

Here's my final render loop:

ui.requireUpdates = 3;
while (!glfwWindowShouldClose(ui.window))
{
	auto now = sclock::now();
	ui.deltaTime = dt(ui.renderTime, now)/1000.0f;
	ui.renderTime = now;
	if (ui.requireUpdates)
	{
		ui.requireUpdates--;
		ui.UpdateUI();
		ui.RenderUI(true);
	}
	else if (ui.requireRender)
	{ // Render parts of the screen with OnDemand rendered items
		ui.RenderUI(false);
	}
	ui.requireRender = false;

	long targetIntervalUS = 1000000/60; // RenderUI (glfwSwapBuffer) might force the frequency down to display refresh rate
	long curIntervalUS = dtUS(ui.renderTime, sclock::now());
	if (curIntervalUS < targetIntervalUS)
	{
		std::this_thread::sleep_for(std::chrono::microseconds(targetIntervalUS-curIntervalUS));
	}

	glfwPollEvents();
	while (!ui.requireRender && ui.requireUpdates == 0
		&& context->InputEventsQueue.empty())
	{
		glfwWaitEventsTimeout(0.5f/1000.0f);
	}
	if (!context->InputEventsQueue.empty())
		ui.requireUpdates = std::max(ui.requireUpdates, 3);
}

with UpdateUI just being ImGui's stack from NewFrame to Render, and RenderUI:

void RenderUI(bool fullUpdate)
{
	if (fullUpdate)
	{
		// Render 2D UI with callbacks at appropriate places for 3D GL
		ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
	}
	else
	{
		// Render areas of the screen with on-demand items - rest will be discarded
		for (auto &onDemandState : onDemandStack)
			ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData(), onDemandState.clipMin, onDemandState.clipMax);
	}

	glfwMakeContextCurrent(window);
	glfwSwapBuffers(window);
}

The label I mentioned is drawn on-demand like this (which internally uses AddCallback):

AddOnDemandText("Frame 00000", [](const ImDrawList* dl, const ImDrawCmd* dc)
{
	RenderOnDemandText(*(OnDemandState*)dc->UserCallbackData, "Frame %d", GetApp().frameNum);
});
// Instead of:
//ImGui::Text("Frame %d", GetApp().frameNum);

And the GL views are setup something like this:

ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddCallback([](const ImDrawList* dl, const ImDrawCmd* dc)
{
	glViewport(...)
	glScissor(...);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Render GL Scene
}, nullptr);
draw_list->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
MarkOnDemandArea(ImGui::GetCurrentWindowRead()->InnerRect);

Finally, the patch for the imgui_impl_opengl3.cpp aswell as the custom on-demand rendering code is attached.
imgui_onDemand.hpp.txt
imgui_onDemand.cpp.txt
imgui_impl_opengl3.cpp.diff.txt

UPDATE: Moved to a gist: https://gist.github.com/Seneral/b4b34a283539938869cd10b2d065a88c

ocornut added a commit that referenced this pull request 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants