The problem#
Working on the same screensaver as yesterday, we want to minimize CPU usage. Since the screensaver is a clock showing hours and minutes, there's no need to do anything except once a minute to change the time display. Optimally, the process would only be scheduled once a minute, exactly when the minute changed, to draw the screen.
SDL timers#
SDL supports timers through SDL_AddTimer()
.
The first naive attempt is to simply create a timer event to run every
second that checks if the minute has changed and redraw the screen if it
has. This has two problems:
- It wakes up every second instead of every minute.
- It can be up to a second off on when it actually updates the screen.
Both of these can be fixed by adjusting the timer delay based on when the timer function is run. The way timer callbacks work in SDL, the return value of the timer callback is the number of milliseconds until the next time the timer will be run. So, in the timer callback, we compute the number of milliseconds until the next minute and return that:
struct tm *time_i;
timeval tv;
gettimeofday(&tv, NULL);
time_i = localtime(&tv.tv_sec);
int seconds_to_next_minute = 60 - (*time_i)->tm_sec;
int ms_to_next_minute = seconds_to_next_minute*1000 - tv.tv_usec/1000;
This snippet uses gettimeofday()
to get the current
time with microsecond precision and uses localtime()
to
split out the seconds part of the time to determine when the next minute
is.
SDL events#
The catch is that SDL does not have a way to do nothing waiting for an event.
SDL is designed for games and other programs that are constantly
watching for input and updating the screen, so it is built around
an event loop that constantly polls for new
events. SDL_WaitEvent()
checks for new events every
30 milliseconds, which is much more often than we actually need.
Alternatively, you can use SDL_PollEvent()
in a loop
with your own SDL_Delay()
of some amount other than 30
milliseconds.
Since we don't care about reacting quickly to events or handling input,
what we actually want is something more like select()
which
only wakes up the process when there is actually a new event. SDL
does not offer such a mechanism, so we have to devise a workaround.1
Sleeping for a minute#
We don't actually care about input events because XScreenSaver is
handling input for us; therefore, we always know exactly when the next
event we care about will occur. That means we can just use the above
code for determining how long until the next minute and sleep with
SDL_Delay()
which will call nanosleep()
.
This introduces a new problem which, luckily, was
solved yesterday: while sleeping, the program can't
respond to requests to exit. The solution is actually even simpler
than the one proposed yesterday. The default handler for
SIGTERM actually does exactly what we want: the kernel
immediately terminates the program. The only complication is that SDL
inserts its own handler. The fix is reset the handler
after calling SDL_Init
:
signal(SIGTERM, SIG_DFL);
SDL 1 vs. SDL 2#
Watching the CPU time used column in htop
, on SDL 1 this
solution seems to still take more than zero CPU time when waiting.
Porting the code over to SDL 2 made it actually take zero
CPU time except when changing the screen once per minute.
You can get my SDL 2 port of noflipqlo, which is incomplete because the drawing code really should be using OpenGL instead of slowly drawing pixel by pixel.
-
Realistically, the appropriate solution is probably to not use SDL, but I was trying to minimally change this program so I didn't have to rewrite the drawing code. ↩
Comments
Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.
There are no comments yet.