A Weird Imagination

Rarely drawing screen in SDL

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:

  1. It wakes up every second instead of every minute.
  2. 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.


  1. 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.