threaded animation with cairo

Multi-threaded Animation with Cairo and GTK+

Complex animations with cairo and GTK+ can result in a laggy interface. This is because the gtk_main() thread runs in a single loop. So, if your do_draw() function implements a complicated drawing command, and it is called from the gtk_main() thread (say by an on_window_expose_event() function), the rest of your gtk code will be blocked until the do_draw() function finishes. Consequentially, menu items, mouse clicks, and even close button events will be slow to be processed and your interface will feel laggy.

One solution is to hand off all the processor-intensive drawing to a separate thread, thus freeing the gtk_main() thread to respond to events.

This tutorial will show you how to safely implement multi-threaded c code to draw a processor intensive animation to a gtk_window.

GTK+ and Thread Safety

GTK+, by default, is not thread safe. However, with a few commands, it can be made (what the GTK+ documentation calls) thread aware. This basically means that GTK+ works fine in a multithreaded environment as any calls made to a gtk object outside of gtk_main() are protected by gdk_threads_enter() and gdk_threads_leave() commands.[1]

Creating a thread-aware main()

In order to make GTK+ thread aware, we use the g_thread_init(NULL) and gdk_threads_init() commands. After that, any calls to gtk need to be encased between gdk_threads_enter() and gdk_threads_leave(). This includes gtk_main()! (Because gtk_main() is called between gdk_threads_enter() and gdk_threads_leave(), any callback functions are automatically protected, so the gdk_threads_enter() and gdk_threads_leave() functions shouldn't be used again until we launch a different thread.)

A minimal thread-aware gtk program might look like:

#include <gtk/gtk.h>

int main (int argc, char *argv[]){

    //we need to initialize all these functions so that gtk knows
    //to be thread-aware
    if (!g_thread_supported ()){ g_thread_init(NULL); }
    gdk_threads_init();
    gdk_threads_enter();

    gtk_init(&argc, &argv);

    GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    gtk_widget_show_all(window);

    gtk_main();
    gdk_threads_leave();

    return 0;
}

Setting up Callbacks for Animation

We will use a g_timeout_add to call our do_draw() routine at 30 fps. Eventually, our do_draw will draw to a global GdkPixmap and we will paint this pixmap to the screen upon an expose_event. It is considered good practice to draw on a widget during its expose_event only.

#include <gtk/gtk.h>
#include <unistd.h>
#include <pthread.h>

//the global pixmap that will serve as our buffer
static GdkPixmap *pixmap = NULL;

int main (int argc, char *argv[]){


    //we need to initialize all these functions so that gtk knows
    //to be thread-aware
    if (!g_thread_supported ()){ g_thread_init(NULL); }
    gdk_threads_init();
    gdk_threads_enter();

    gtk_init(&argc, &argv);

    GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(window), "expose_event", G_CALLBACK(on_window_expose_event), NULL);
    g_signal_connect(G_OBJECT(window), "configure_event", G_CALLBACK(on_window_configure_event), NULL);

    //this must be done before we define our pixmap so that it can reference
    //the colour depth and such
    gtk_widget_show_all(window);

    //set up our pixmap so it is ready for drawing
    pixmap = gdk_pixmap_new(window->window,500,500,-1);
    //because we will be painting our pixmap manually during expose events
    //we can turn off gtk's automatic painting and double buffering routines.
    gtk_widget_set_app_paintable(window, TRUE);
    gtk_widget_set_double_buffered(window, FALSE);

    (void)g_timeout_add(33, (GSourceFunc)timer_exe, window);


    gtk_main();
    gdk_threads_leave();

    return 0;
}

Timer Function

Our timer function will be responsible for launching a new thread that executes our do_draw() function. It will then artifically send an expose event to our window so that it knows it should redraw (sending an expose event rather than doing the drawing ourself will allow gtk to process events closer tot he way it wants).

Before, we implement our timer function, we must consider one important issue. If our drawing application is going to take a long time (longer than 1/30th of a second) then simply launching a new drawing thread every time the timer executes could result in a pile-up of threads and a lot of memory badness. To solve this problem, we will have a variable currently_drawing. If currently_drawing=0 then it is safe to launch a drawing thread. If it isn't, we know we haven't finished the drawing we started last time! Using this solution instead of sending a signal from our thread when it finishes, results in simpler code and has the added benefit that our framerate is limited to whatever rate our timer is called at.

gboolean timer_exe(GtkWidget * window){

    static gboolean first_execution = TRUE;

    //use a safe function to get the value of currently_drawing so
    //we don't run into the usual multithreading issues
    int drawing_status = g_atomic_int_get(&currently_drawing);

    //if we are not currently drawing anything, launch a thread to 
    //update our pixmap
    if(drawing_status == 0){
        static pthread_t thread_info;
        int  iret;
        if(first_execution != TRUE){
            pthread_join(thread_info, NULL);
        }
        iret = pthread_create( &thread_info, NULL, do_draw, NULL);
    }

    //tell our window it is time to draw our animation.
    int width, height;
    gdk_drawable_get_size(pixmap, &width, &height);
    gtk_widget_queue_draw_area(window, 0, 0, width, height);

    first_execution = FALSE;

    return TRUE;

}

Do the Drawing

We now have a timer_exe function that launches our do_draw() in a new thread for us. Now lets implement do_draw(). Because we are now in a thread that is running outside of gtk_main(), we need to encase any calls involving gtk objects between gdk_threads_enter() and gdk_threads_leave(). This includes interactions with GdkPixmaps!

Note: the gtk_main() thread will be blocked until the code between gdk_threads_enter() and gdk_threads_leave() has executed, so don't put avoidable, processor-intensive code there.

Our goal is to draw with cairo and have our drawing end up in pixmap, our global pixmap to be drawn upon expose_event. However, any cairo context created from pixmap must be accessed between gdk_threads_enter() and gdk_threads_leave(). If we did all our drawing this way, it would defeat the purpose of multi-threading, as we would have no speedup. We need to create a cairo context that that is independent from GTK so that we may draw upon it without a lock. After we have done the processor-intensive drawing, we can do the relatively-quick operation of copying our drawing to pixmap between gdk_threads_enter() and gdk_threads_leave().

The solution comes from cairo_image_surface_create(), which will create an area in memory from which we can create a cairo context that can be drawn upon without fear of thread issues.

static int currently_drawing = 0;

//do_draw will be executed in a separate thread whenever we would like to update
//our animation
void *do_draw(void *ptr){

    currently_drawing = 1;

    int width, height;
    gdk_threads_enter();
    gdk_drawable_get_size(pixmap, &width, &height);
    gdk_threads_leave();

    //create a gtk-independant surface to draw on
    cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
    cairo_t *cr = cairo_create(cst);

    /* do all your drawing here */

    cairo_destroy(cr);


    //When dealing with gdkPixmap's, we need to make sure not to
    //access them from outside gtk_main().
    gdk_threads_enter();

    cairo_t *cr_pixmap = gdk_cairo_create(pixmap);
    cairo_set_source_surface (cr_pixmap, cst, 0, 0);
    cairo_paint(cr_pixmap);
    cairo_destroy(cr_pixmap);

    gdk_threads_leave();

    cairo_surface_destroy(cst);

    currently_drawing = 0;

    return NULL;
}

The Callbacks

The last thing we need to do is handle our callbacks. These are on_window_configure_event() where we will hand resize-events and on_window_expose_event() where we will paint pixmap to the screen. The only thing of note is in on_window_configure_event(). Here we create a new pixmap of the same size as our window. We then delete the old one. Doing exactly these steps in this order would result in a bunch of garbage being painted on the screen (because the new pixmap we create is not initialized). Since our drawing operation may take quite a while, we minimize the uglyness by copying the contens of our old pixmap to our new pixmap before we destroy the old one.

gboolean on_window_configure_event(GtkWidget * da, GdkEventConfigure * event, gpointer user_data){
    static int oldw = 0;
    static int oldh = 0;
    //make our selves a properly sized pixmap if our window has been resized
    if (oldw != event->width || oldh != event->height){
        //create our new pixmap with the correct size.
        GdkPixmap *tmppixmap = gdk_pixmap_new(da->window, event->width,  event->height, -1);
        //copy the contents of the old pixmap to the new pixmap.  This keeps ugly uninitialized
        //pixmaps from being painted upon resize
        int minw = oldw, minh = oldh;
        if( event->width < minw ){ minw =  event->width; }
        if( event->height < minh ){ minh =  event->height; }
        gdk_draw_drawable(tmppixmap, da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap, 0, 0, 0, 0, minw, minh);
        //we're done with our old pixmap, so we can get rid of it and replace it with our properly-sized one.
        g_object_unref(pixmap); 
        pixmap = tmppixmap;
    }
    oldw = event->width;
    oldh = event->height;
    return TRUE;
}

gboolean on_window_expose_event(GtkWidget * da, GdkEventExpose * event, gpointer user_data){
    gdk_draw_drawable(da->window,
        da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap,
        // Only copy the area that was exposed.
        event->area.x, event->area.y,
        event->area.x, event->area.y,
        event->area.width, event->area.height);
    return TRUE;
}

Compiling

To compile, use pkg-config --cflags --libs gtk+-2.0 --libs gthread-2.0 For example, if you save this code to threaded_examp.c, you would compile it with the command:

gcc threaded_examp.c `pkg-config --cflags --libs gtk+-2.0 --libs gthread-2.0` -Wall -o threaded_examp

Full Source

For your compiling pleasure, the full source in proper order.

#include <gtk/gtk.h>
#include <unistd.h>
#include <pthread.h>

//the global pixmap that will serve as our buffer
static GdkPixmap *pixmap = NULL;

gboolean on_window_configure_event(GtkWidget * da, GdkEventConfigure * event, gpointer user_data){
    static int oldw = 0;
    static int oldh = 0;
    //make our selves a properly sized pixmap if our window has been resized
    if (oldw != event->width || oldh != event->height){
        //create our new pixmap with the correct size.
        GdkPixmap *tmppixmap = gdk_pixmap_new(da->window, event->width,  event->height, -1);
        //copy the contents of the old pixmap to the new pixmap.  This keeps ugly uninitialized
        //pixmaps from being painted upon resize
        int minw = oldw, minh = oldh;
        if( event->width < minw ){ minw =  event->width; }
        if( event->height < minh ){ minh =  event->height; }
        gdk_draw_drawable(tmppixmap, da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap, 0, 0, 0, 0, minw, minh);
        //we're done with our old pixmap, so we can get rid of it and replace it with our properly-sized one.
        g_object_unref(pixmap); 
        pixmap = tmppixmap;
    }
    oldw = event->width;
    oldh = event->height;
    return TRUE;
}

gboolean on_window_expose_event(GtkWidget * da, GdkEventExpose * event, gpointer user_data){
    gdk_draw_drawable(da->window,
        da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap,
        // Only copy the area that was exposed.
        event->area.x, event->area.y,
        event->area.x, event->area.y,
        event->area.width, event->area.height);
    return TRUE;
}


static int currently_drawing = 0;
//do_draw will be executed in a separate thread whenever we would like to update
//our animation
void *do_draw(void *ptr){

    currently_drawing = 1;

    int width, height;
    gdk_threads_enter();
    gdk_drawable_get_size(pixmap, &width, &height);
    gdk_threads_leave();

    //create a gtk-independant surface to draw on
    cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
    cairo_t *cr = cairo_create(cst);

    //do some time-consuming drawing
    static int i = 0;
    ++i; i = i % 300;   //give a little movement to our animation
    cairo_set_source_rgb (cr, .9, .9, .9);
    cairo_paint(cr);
    int j,k;
    for(k=0; k<100; ++k){   //lets just redraw lots of times to use a lot of proc power
        for(j=0; j < 1000; ++j){
            cairo_set_source_rgb (cr, (double)j/1000.0, (double)j/1000.0, 1.0 - (double)j/1000.0);
            cairo_move_to(cr, i,j/2); 
            cairo_line_to(cr, i+100,j/2);
            cairo_stroke(cr);
        }
    }
    cairo_destroy(cr);


    //When dealing with gdkPixmap's, we need to make sure not to
    //access them from outside gtk_main().
    gdk_threads_enter();

    cairo_t *cr_pixmap = gdk_cairo_create(pixmap);
    cairo_set_source_surface (cr_pixmap, cst, 0, 0);
    cairo_paint(cr_pixmap);
    cairo_destroy(cr_pixmap);

    gdk_threads_leave();

    cairo_surface_destroy(cst);

    currently_drawing = 0;

    return NULL;
}

gboolean timer_exe(GtkWidget * window){

    static gboolean first_execution = TRUE;

    //use a safe function to get the value of currently_drawing so
    //we don't run into the usual multithreading issues
    int drawing_status = g_atomic_int_get(&currently_drawing);

    //if we are not currently drawing anything, launch a thread to 
    //update our pixmap
    if(drawing_status == 0){
        static pthread_t thread_info;
        int  iret;
        if(first_execution != TRUE){
            pthread_join(thread_info, NULL);
        }
        iret = pthread_create( &thread_info, NULL, do_draw, NULL);
    }

    //tell our window it is time to draw our animation.
    int width, height;
    gdk_drawable_get_size(pixmap, &width, &height);
    gtk_widget_queue_draw_area(window, 0, 0, width, height);

    first_execution = FALSE;

    return TRUE;

}


int main (int argc, char *argv[]){


    //we need to initialize all these functions so that gtk knows
    //to be thread-aware
    if (!g_thread_supported ()){ g_thread_init(NULL); }
    gdk_threads_init();
    gdk_threads_enter();

    gtk_init(&argc, &argv);

    GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(window), "expose_event", G_CALLBACK(on_window_expose_event), NULL);
    g_signal_connect(G_OBJECT(window), "configure_event", G_CALLBACK(on_window_configure_event), NULL);

    //this must be done before we define our pixmap so that it can reference
    //the colour depth and such
    gtk_widget_show_all(window);

    //set up our pixmap so it is ready for drawing
    pixmap = gdk_pixmap_new(window->window,500,500,-1);
    //because we will be painting our pixmap manually during expose events
    //we can turn off gtk's automatic painting and double buffering routines.
    gtk_widget_set_app_paintable(window, TRUE);
    gtk_widget_set_double_buffered(window, FALSE);

    (void)g_timeout_add(33, (GSourceFunc)timer_exe, window);


    gtk_main();
    gdk_threads_leave();

    return 0;
}

Alternative Method Using Signals

The previous example has the potential to create a new thread each time the timer_exe function is called. And, although threads in linux are lightweight, we can make drawing with high framerates more efficient by creating one drawing thread and sending it a signal every time we want it to update. To do this, we will need to #include <signal.h>. We then will tell our application to watch for SIGALRM and we will set up a sigwaitinfo in our drawing thread to block until it recieves a SIGALRM.

Below is the full source of threaded_examp, but with signals used to trigger drawing:

#include <gtk/gtk.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

//the global pixmap that will serve as our buffer
static GdkPixmap *pixmap = NULL;

gboolean on_window_configure_event(GtkWidget * da, GdkEventConfigure * event, gpointer user_data){
    static int oldw = 0;
    static int oldh = 0;
    //make our selves a properly sized pixmap if our window has been resized
    if (oldw != event->width || oldh != event->height){
        //create our new pixmap with the correct size.
        GdkPixmap *tmppixmap = gdk_pixmap_new(da->window, event->width,  event->height, -1);
        //copy the contents of the old pixmap to the new pixmap.  This keeps ugly uninitialized
        //pixmaps from being painted upon resize
        int minw = oldw, minh = oldh;
        if( event->width < minw ){ minw =  event->width; }
        if( event->height < minh ){ minh =  event->height; }
        gdk_draw_drawable(tmppixmap, da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap, 0, 0, 0, 0, minw, minh);
        //we're done with our old pixmap, so we can get rid of it and replace it with our properly-sized one.
        g_object_unref(pixmap); 
        pixmap = tmppixmap;
    }
    oldw = event->width;
    oldh = event->height;
    return TRUE;
}

gboolean on_window_expose_event(GtkWidget * da, GdkEventExpose * event, gpointer user_data){
    gdk_draw_drawable(da->window,
        da->style->fg_gc[GTK_WIDGET_STATE(da)], pixmap,
        // Only copy the area that was exposed.
        event->area.x, event->area.y,
        event->area.x, event->area.y,
        event->area.width, event->area.height);
    return TRUE;
}


static int currently_drawing = 0;
//do_draw will be executed in a separate thread whenever we would like to update
//our animation
void *do_draw(void *ptr){

    //prepare to trap our SIGALRM so we can draw when we recieve it!
    siginfo_t info;
    sigset_t sigset;

    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);

    while(1){
        //wait for our SIGALRM.  Upon receipt, draw our stuff.  Then, do it again!
        while (sigwaitinfo(&sigset, &info) > 0) {

            currently_drawing = 1;

            int width, height;
            gdk_threads_enter();
            gdk_drawable_get_size(pixmap, &width, &height);
            gdk_threads_leave();

            //create a gtk-independant surface to draw on
            cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
            cairo_t *cr = cairo_create(cst);

            //do some time-consuming drawing
            static int i = 0;
            ++i; i = i % 300;   //give a little movement to our animation
            cairo_set_source_rgb (cr, .9, .9, .9);
            cairo_paint(cr);
            int j,k;
            for(k=0; k<100; ++k){   //lets just redraw lots of times to use a lot of proc power
                for(j=0; j < 1000; ++j){
                    cairo_set_source_rgb (cr, (double)j/1000.0, (double)j/1000.0, 1.0 - (double)j/1000.0);
                    cairo_move_to(cr, i,j/2); 
                    cairo_line_to(cr, i+100,j/2);
                    cairo_stroke(cr);
                }
            }
            cairo_destroy(cr);


            //When dealing with gdkPixmap's, we need to make sure not to
            //access them from outside gtk_main().
            gdk_threads_enter();

            cairo_t *cr_pixmap = gdk_cairo_create(pixmap);
            cairo_set_source_surface (cr_pixmap, cst, 0, 0);
            cairo_paint(cr_pixmap);
            cairo_destroy(cr_pixmap);

            gdk_threads_leave();

            cairo_surface_destroy(cst);

            currently_drawing = 0;

        }
    }
}

gboolean timer_exe(GtkWidget * window){
    static int first_time = 1;
    //use a safe function to get the value of currently_drawing so
    //we don't run into the usual multithreading issues
    int drawing_status = g_atomic_int_get(&currently_drawing);

    //if this is the first time, create the drawing thread
    static pthread_t thread_info;
    if(first_time == 1){
        int  iret;
        iret = pthread_create( &thread_info, NULL, do_draw, NULL);
    }

    //if we are not currently drawing anything, send a SIGALRM signal
    //to our thread and tell it to update our pixmap
    if(drawing_status == 0){
        pthread_kill(thread_info, SIGALRM);
    }

    //tell our window it is time to draw our animation.
    int width, height;
    gdk_drawable_get_size(pixmap, &width, &height);
    gtk_widget_queue_draw_area(window, 0, 0, width, height);


    first_time = 0;
    return TRUE;

}


int main (int argc, char *argv[]){

    //Block SIGALRM in the main thread
    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);
    pthread_sigmask(SIG_BLOCK, &sigset, NULL);

    //we need to initialize all these functions so that gtk knows
    //to be thread-aware
    if (!g_thread_supported ()){ g_thread_init(NULL); }
    gdk_threads_init();
    gdk_threads_enter();

    gtk_init(&argc, &argv);

    GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(window), "expose_event", G_CALLBACK(on_window_expose_event), NULL);
    g_signal_connect(G_OBJECT(window), "configure_event", G_CALLBACK(on_window_configure_event), NULL);

    //this must be done before we define our pixmap so that it can reference
    //the colour depth and such
    gtk_widget_show_all(window);

    //set up our pixmap so it is ready for drawing
    pixmap = gdk_pixmap_new(window->window,500,500,-1);
    //because we will be painting our pixmap manually during expose events
    //we can turn off gtk's automatic painting and double buffering routines.
    gtk_widget_set_app_paintable(window, TRUE);
    gtk_widget_set_double_buffered(window, FALSE);

    (void)g_timeout_add(33, (GSourceFunc)timer_exe, window);


    gtk_main();
    gdk_threads_leave();

    return 0;
}

References

[1] http://research.operationaldynamics.com/blogs/andrew/software/gnome-desktop/gtk-thread-awareness.html

[2] http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html