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

[GUI] Support gui.fps_limit and reduce idle power consumption #1611

Merged
merged 20 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/gui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,19 @@ A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e.
mouse_x, mouse_y = gui.get_cursor_pos()


.. attribute:: gui.fps_limit

:parameter gui: (GUI)
:return: (scalar or None) the maximum FPS, ``None`` for no limit

The default value is 60.

For example, to restrict FPS to be below 24, simply ``gui.fps_limit = 24``.
This helps reduce the overload on your hardware especially when you're
using OpenGL on your intergrated GPU which could make desktop slow to
response.


GUI Widgets
-----------

Expand Down
3 changes: 2 additions & 1 deletion examples/game_of_life.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def render():


gui = ti.GUI('Game of Life', (img_size, img_size))
gui.fps_limit = 15

print('[Hint] Press `r` to reset')
print('[Hint] Press SPACE to pause')
Expand All @@ -87,7 +88,7 @@ def render():
alive[int(mx * n), int(my * n)] = gui.is_pressed(gui.LMB)
paused = True

if not paused and gui.frame % 4 == 0:
if not paused:
run()

render()
Expand Down
14 changes: 14 additions & 0 deletions python/taichi/misc/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,20 @@ def running(self, value):
elif not self.core.should_close:
self.core.should_close = 1

@property
def fps_limit(self):
if self.core.frame_delta_limit == 0:
return None
else:
return 1 / self.core.frame_delta_limit

@fps_limit.setter
def fps_limit(self, value):
if value is None:
self.core.frame_delta_limit = 0
else:
self.core.frame_delta_limit = 1 / value


def rgb_to_hex(c):
to255 = lambda x: np.clip(np.int32(x * 255), 0, 255)
Expand Down
10 changes: 5 additions & 5 deletions taichi/gui/gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ class GUI : public GUIBase {
std::string window_name;
int width, height;
int frame_id = 0;
const int fps = 60;
real frame_delta_limit = 1.0 / 60;
float64 start_time;
Array2D<Vector4> buffer;
std::vector<real> last_frame_interval;
Expand Down Expand Up @@ -806,12 +806,12 @@ class GUI : public GUIBase {
void update() {
frame_id++;
redraw_widgets();
while (taichi::Time::get_time() < last_frame_time + 1 / (real)fps)
;
taichi::Time::wait_until(last_frame_time + frame_delta_limit);
auto this_frame_time = taichi::Time::get_time();
if (last_frame_time != 0) {
last_frame_interval.push_back(taichi::Time::get_time() - last_frame_time);
last_frame_interval.push_back(this_frame_time - last_frame_time);
}
last_frame_time = taichi::Time::get_time();
last_frame_time = this_frame_time;
redraw();
// Some old examples / users don't even provide a `break` statement for us
// to terminate loop. So we have to terminate the program with RuntimeError
Expand Down
1 change: 1 addition & 0 deletions taichi/python/export_visual.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void export_visual(py::module &m) {
.value("Release", Type::release);
py::class_<GUI>(m, "GUI")
.def(py::init<std::string, Vector2i>())
.def_readwrite("frame_delta_limit", &GUI::frame_delta_limit)
.def_readwrite("should_close", &GUI::should_close)
.def("get_canvas", &GUI::get_canvas, py::return_value_policy::reference)
.def("set_img",
Expand Down
66 changes: 65 additions & 1 deletion taichi/system/timer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,82 @@ double Time::get_time() {
}
#endif

#ifdef _WIN64
#include <Windows.h>
#pragma comment(lib, "Winmm.lib")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have added win_usleep , win_msleep for different use cases on Windows .
None behavior changes on Linux. You can try to run on your machine to check if anything is different : )

Nice work! I can confirm that it's running happily on my machine!
One concern, what is Winmm.lib? Will it cause compat issue?
We'd better include it in CMakeList.txt instead of an inline #pragma comment, WDYT?
I'd like to provide more usage about linking libraries in CMake if you are not familiar to.

Copy link
Contributor

@JYLeeLYJ JYLeeLYJ Aug 4, 2020

Choose a reason for hiding this comment

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

Winmm.lib is just a system multimedia API , which contains high resolution timer event functions .
Winmm.dll could be found in system path . And the minimum support client is winXP . This might not cause incompatibility problem.
See : MSDN

At the last commit I have moved #pragma commit away and used target_link_library instead .


void win_usleep(double us) {
using us_t = chrono::duration<double, std::micro>;
auto start = chrono::high_resolution_clock::now();
do {
// still little possible to release cpu.
// Note:
// https://docs.microsoft.com/zh-cn/windows/win32/api/synchapi/nf-synchapi-sleep
Sleep(0);
} while ((us_t(chrono::high_resolution_clock::now() - start).count()) < us);
}

void win_msleep(DWORD ms) {
if (ms == 0)
Sleep(0);
else {
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
timeSetEvent(ms, 1, (LPTIMECALLBACK)hEvent, 0,
TIME_ONESHOT | TIME_CALLBACK_EVENT_SET);
WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);
}
}
#endif

void Time::usleep(double us) {
#ifdef _WIN64
Sleep(DWORD(us * 1e-3));
// use win_usleep for accuracy.
if (us < 999)
win_usleep(us);
// use win_msleep to release cpu, precision < 1ms
else
win_msleep(DWORD(us * 1e-3));
#else
::usleep(us);
#endif
}

void Time::msleep(double ms) {
#ifdef _WIN64
win_msleep(DWORD(ms));
#else
::usleep(ms * 1e3_f64);
#endif
}

void Time::sleep(double s) {
Time::usleep(s * 1e6_f64);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here is a mush more accurate milliseconds level sleep implementation instead of Sleep(DWORD(us * 1e-3)) on windows .

#ifdef _WIN64

#include <Windows.h>
#pragma comment(lib, "Winmm.lib")

void win_sleep(DWORD ms) {
  if (ms == 0)
    Sleep(0);
  else {
    HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    timeSetEvent(ms, 1, (LPTIMECALLBACK)hEvent , 0, TIME_ONESHOT | TIME_CALLBACK_EVENT_SET);
    WaitForSingleObject(hEvent, INFINITE);
    CloseHandle(hEvent);
  }
}
#endif

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds cool! But is that possible to sleep for microseconds (us) on Windows?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds cool! But is that possible to sleep for microseconds (us) on Windows?

It's sad truth that sleep accuracy can not be guaranteed when delay time is lower than 1ms :(
The only possible approach is to use high resolution clock to continually counting in a for loop , but it 's unable to release CPU time slice ...

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sleeping for us seems tricky on win, how about just:

#ifndef _WIN64
} while (dt > 1e-4_f64);  // until dt <= 100us
#else
} while (dt > 1e-2_f64);  // until dt <= 10ms
#endif

Copy link
Contributor

@JYLeeLYJ JYLeeLYJ Jul 31, 2020

Choose a reason for hiding this comment

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

1ms is more accurate ( 1/60 = 16.6 ms , 1/120 = 8.3ms , 1/40 = 25 ms )

#else
} while (dt > 1e-3_f64);  // until dt <= 1ms
#endif


void Time::wait_until(double t) {
// microsecond (us) sleep on Windows... sadly.
double dt;
if (t < Time::get_time()) {
return;
}
do { // use system-provided sleep for large scale sleeping:
dt = t - Time::get_time();
if (dt <= 0) {
return;
}
#ifdef _WIN64
Time::sleep(dt * 0.5);
#else
Time::sleep(dt * (dt < 4e-2_f64 ? 0.02 : 0.4));
Copy link
Contributor

@JYLeeLYJ JYLeeLYJ Aug 1, 2020

Choose a reason for hiding this comment

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

Is any difference here ( using 0.02 or 0.4 )?
Why not just use :

Suggested change
Time::sleep(dt * (dt < 4e-2_f64 ? 0.02 : 0.4));
Time::sleep(dt * 0.4);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This ensures that the dt of sleep is more accurate when small; without this, I found the FPS to be around 59; with this, I found the FPS to be accurately 60.00.

#endif
archibate marked this conversation as resolved.
Show resolved Hide resolved
} while (dt > 2e-4_f64); // until dt <= 200us

// use an EBFE loop for small scale waiting:
while (Time::get_time() < t - 1e-6_f64)
; // until dt <= 1us
}

double Time::Timer::get_time() {
return Time::get_time();
}
Expand Down
3 changes: 2 additions & 1 deletion taichi/system/timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ TI_NAMESPACE_BEGIN
class Time {
public:
static double get_time();

static uint64 get_cycles();
static void wait_until(double t);

static void usleep(double us);
static void msleep(double ms);
static void sleep(double s);

class Timer {
Expand Down