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;
}
g_thread_init(NULL)
starts a bunch of threading preparation. This functions should only be called once in any given program. Consequentially, it is called asif (!g_thread_supported ()){ g_thread_init(NULL); }
This is not strictly necessary (because we are fully aware thatg_threads_init()
is called nowhere else in our code; however, when integrating with other projects, it is a good safety precaution.g_signal_connect(G_OBJECT(window), "configure_event", G_CALLBACK(on_window_configure_event), NULL)
adds a callback to the configure event so that we can detect and handle resizes.gtk_widget_set_app_paintable(window, TRUE)
andgtk_widget_set_double_buffered(window, FALSE)
tells gtk that we will be doing our own buffering of the window(void)g_timeout_add(33, (GSourceFunc)timer_exe, window)
adds a timer that will be executed bygtk_main()
30 times a second (unlessgtk_main()
is too busy with other things). We pass it the name of the function we'd like to calltimer_exe
cast as aGSourceFunc
and we also pass it a pointer to the object we'd like to draw on, this timewindow
.
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(¤tly_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;
}
g_atomic_int_get(¤tly_drawing)
is a thread-safe way to get the value of our global integercurrently_drawing
. Using this function allows us to avoid the possibility of reading a number at the same moment our other thread is trying to change it. It is also much easier to implement than mutexes for reading a single integer.pthread_create( &thread_info, NULL, do_draw, NULL)
is theunistd.h
way to launch the functiondo_draw()
as a separate thread. (The finalNULL
is actually a(void *)
to a data structure that we pass todo_draw()
.)gtk_widget_queue_draw_area(window, 0, 0, width, height)
sends an artificial expose event with upper left corner 0,0 and width and heigh ofwidth
,height
, respectively. This allows ourexpose_event
to do the actual painting.pthread_join(thread_info, NULL)
re-joins the drawing thread and ensures that it terminates and it's OS-related memory is freed.
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 GdkPixmap
s!
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;
}
currently_drawing
is a global variable that tellstimer_exe
whether or not we've finishedgdk_drawable_get_size(pixmap, &width, &height)
is used to get the size ofpixmap
, our eventual target so that we create a cairo context of equivalent size. Note that our call togdk_drawable_get_size()
is encased betweengdk_threads_enter()
andgdk_threads_leave()
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height)
creates a region in memory suitable for a 32-bit image of widthwidth
and heightheight
.cairo_create(cst)
creates a cairo context out of our memmap so that we may draw upon it.cairo_destroy(cr)
destroys the cairo context we used. We can safely destroy the drawing context as soon as we've finished drawing. We don't want to forget this and have a memory leak.gdk_cairo_create(pixmap)
creates a cairo context out ofpixmap
so that we can draw the contents ofcst
onto it. Note that this is done betweengdk_threads_enter()
andgdk_threads_leave()
.cairo_set_source_surface (cr_pixmap, cst, 0, 0)
will copy the contents ofcst
to the upper left corner ofcr_pixmap
, which is the cairo context currently associated withpixmap
.
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(¤tly_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(¤tly_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