diff --git a/Makefile b/Makefile index 567ef6e..949dd1e 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ VERSION = 0.0 PREFIX = /usr/local MANPREFIX = $(PREFIX)/share/man -CFLAGS = `pkg-config --cflags gtk4` -LDFLAGS = `pkg-config --libs gtk4` -lm +CFLAGS = `pkg-config --cflags gtk4` -std=c99 -Wall -Wextra -g -O0 # -pedantic +LDFLAGS = `pkg-config --libs gtk4` -lm -lpthread -CC = mpicc +CC = gcc SRC = visor.c OBJ = ${SRC:.c=.o} diff --git a/README.md b/README.md index 61acac0..6e18118 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Mandelbrot visualiser -A simple GTK application for drawing the mandelbrot set, meant to showcase some features of OpenMPI. +A simple GTK application for drawing the mandelbrot set. + +This was originally written to showcase some features of OpenMPI for a school +project, I've since rewritten it using pthreads. -This was written for a faculty project, and I will likely rewrite it to use pthreads soon. diff --git a/visor.c b/visor.c index 68ac1a9..ec577e2 100644 --- a/visor.c +++ b/visor.c @@ -1,30 +1,59 @@ -#include -#include +#include +#include +#include +#include #include +#include -int procRank, commSz; -MPI_Datatype planeView_t; +#include +#include -MPI_Win window; +#define DEFAULT_THREADS 4 +#define MAX_THREADS 32 +#define STR(x) #x -unsigned char *pixmap; +int32_t thread_count; + +struct threadInfo { + pthread_t id; + int32_t index; // unique number from 0 to thread_count-1 + bool drawing; // marks if we are currently drawing for a given thread + bool complete; // marks if the drawing that it was supposed to +}; + +struct threadInfo threads[MAX_THREADS]; // this will store an array with status info for all the threads cairo_surface_t *surface = NULL; -#define SCR_DIMENSION 1000 +/* + * Concurrency model explained: + * One reader thread (the GUI thread) of the pixel buffer, along with many + * writers, who split the work of rendering roughly equally. The writers don't + * do any work until the pixel map which they are meant to work on is marked as + * available. + * + * Once it is marked as such, this means that the planeView structure is well + * defined, that pixmap points to a memory region, which is allocated with + * enough memory for the plain view, and they can all start writing without an + * issue. + */ +pthread_mutex_t pixmapMutex; +pthread_cond_t pixmapCond; +bool pixmapAvailable = false; +unsigned char *pixmap; struct planeView { double scale; // scale is represented as unit/pixel int width, height; double centerX, centerY; }; -struct planeView mandelbrot = { 0.00000000001, 0, 0, 0.001643721971153, 0.822467633298876}; -// struct planeView mandelbrot = { 1, 0, 0, 0, 0}; +// struct planeView mandelbrot = { 0.00000000001, 0, 0, 0.001643721971153, 0.822467633298876}; +struct planeView mandelbrot = { 1, 0, 0, 0, 0}; #define MAX_ITERATION 1000 #define ESC_RAD 20.0 -int color_lookup(int *r, int *g, int *b, double mu) +void color_lookup(int *r, int *g, int *b, double mu) { static const int table16[16][3] = { { 66, 30, 15 }, @@ -81,29 +110,86 @@ void color_from_iteration(int *r, int *g, int *b, double x0, double y0) #undef MAX_ITERATION } -void draw_mandelbrot(unsigned char* data, int height, int width, double planeCenterX, double planeCenterY, double scale) +void draw_mandelbrot(int32_t threadIndex) { - for (int i = 0; i < height; i++) { - double yRange = (double) i / height * 2.0 - 1.0; - double y = planeCenterY+yRange*scale; - for (int j = 0; j < width; j++) { - double xRange = (double) j / width * 2.0 - 1.0; - double x = planeCenterX+xRange*scale; + int h = mandelbrot.height; + int w = mandelbrot.width; + int p = threadIndex, c = thread_count; + int mod = h % c, div = h / c; + int a = MIN(p, mod)*(div+1) + (MAX(p, mod)-mod)*div, + b = p < mod ? a + div + 1 : a + div; + // [a..b) is the range of rows a given thread is meant to write pixels + + double centerX = mandelbrot.centerX; + double centerY = mandelbrot.centerY; + double scale = mandelbrot.scale; + + for (int i = a; i < b; i++) { + double yRange = (double) i / h * 2.0 - 1.0; + double y = centerY+yRange*scale; + for (int j = 0; j < w; j++) { + pthread_mutex_lock(&pixmapMutex); + if (!pixmapAvailable) { + pthread_mutex_unlock(&pixmapMutex); + return; + // after this, the thread will again enter the + // loop in the writer_thread function, and + // enter a wait state + } + pthread_mutex_unlock(&pixmapMutex); + + double xRange = (double) j / w * 2.0 - 1.0; + double x = centerX+xRange*scale; + int r, g, b; color_from_iteration(&r, &g, &b, x, y); - data[4*(i*width + j) + 2] = r; - data[4*(i*width + j) + 1] = g; - data[4*(i*width + j) + 0] = b; + pixmap[4*(i*w + j) + 2] = r; + pixmap[4*(i*w + j) + 1] = g; + pixmap[4*(i*w + j) + 0] = b; } } + threads[threadIndex].complete = true; +} + +void *writer_thread(void *index_ptr) +{ + int32_t index = *(int32_t *)index_ptr; + while (true) { + pthread_mutex_lock(&pixmapMutex); + while(!pixmapAvailable || threads[index].complete) { + pthread_cond_wait(&pixmapCond, &pixmapMutex); + } + pthread_mutex_unlock(&pixmapMutex); + threads[index].drawing = true; + draw_mandelbrot(index); + threads[index].drawing = false; + } } void create_surface(GtkWidget *widget) { cairo_t *cr; - if (surface) + pthread_mutex_lock(&pixmapMutex); + pixmapAvailable = false; // Mark drawing area as unavailable + pthread_mutex_unlock(&pixmapMutex); + + bool allWritersStopped; + do { // Wait until all writing threads have been stopped + allWritersStopped = true; + for (int32_t i = 0; i < thread_count; i++) { + if (threads[i].drawing) { + allWritersStopped = false; + } + } + } while (!allWritersStopped); + for (int32_t i = 0; i < thread_count; i++) { + threads[i].complete = false; + } + + if (surface) { cairo_surface_destroy(surface); + } surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, gtk_widget_get_width(widget), @@ -116,30 +202,22 @@ void create_surface(GtkWidget *widget) int w = cairo_image_surface_get_width(surface); mandelbrot.height = h; mandelbrot.width = w; - for (int p = 1; p < commSz; p++) { - MPI_Send(&mandelbrot, 1, planeView_t, p, 0, MPI_COMM_WORLD); - } - pixmap = malloc(sizeof(unsigned char) * 4 * w * h); - MPI_Win_create(pixmap, 4*w*h, 1, - MPI_INFO_NULL, MPI_COMM_WORLD, &window); - MPI_Win_fence(0, window); - - unsigned char *data = cairo_image_surface_get_data(surface); - // draw_mandelbrot(data, h, w, mandelbrot.centerX, mandelbrot.centerY, mandelbrot.scale); - - - MPI_Win_fence(0, window); - memcpy(data, pixmap, 4*h*w); + pixmap = cairo_image_surface_get_data(surface); cairo_surface_mark_dirty(surface); - // cairo_paint(cr); + + pthread_mutex_lock(&pixmapMutex); + pixmapAvailable = true; + pthread_mutex_unlock(&pixmapMutex); + pthread_cond_broadcast(&pixmapCond); cairo_destroy(cr); } -void plane_resize(GtkWidget *widget, int width, int height) +void plane_resize(GtkWidget *widget) { - if (!surface) + if (!surface) { create_surface(widget); + } } void draw_plane(GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer data) @@ -199,81 +277,79 @@ static void app_activate(GApplication *app) gtk_window_present(GTK_WINDOW(win)); } - +void print_usage(FILE *stream) +{ + char *err_msg = "Mandelbrot visualiser (visor):\n" + "Usage:\n" + "visor -h : Show this help\n" + "visor [threads] : Run the GTK visualizer with a given number\n" + " of threads (default " STR(DEFAULT_THREADS) ")\n" + " [1.." STR(MAX_THREADS) "]\n"; + fprintf(stream, "%s", err_msg); +} int main(int argc, char **argv) { - MPI_Init(NULL, NULL); + pthread_mutex_init(&pixmapMutex, NULL); + pthread_cond_init(&pixmapCond, NULL); - MPI_Comm_rank(MPI_COMM_WORLD, &procRank); - MPI_Comm_size(MPI_COMM_WORLD, &commSz); - - int blocklengths[5] = {1,1,1,1,1}; - const MPI_Aint displs[] = {0, sizeof(double), sizeof(double)+sizeof(int), - sizeof(double)+2*sizeof(int), 2*sizeof(double)+2*sizeof(int)}; - MPI_Datatype types[5] = {MPI_DOUBLE, MPI_INT, MPI_INT, MPI_DOUBLE, MPI_DOUBLE}; - MPI_Type_create_struct(5, blocklengths, displs, types, &planeView_t); - MPI_Type_commit(&planeView_t); - - if (commSz == 1) { - printf("This application needs to be run with more than one" - " process. Try using mpiexec.\n"); - MPI_Finalize(); - return -1; - } - - if (procRank == 0) { - int stat = 0; - - GtkApplication *app; - app = gtk_application_new("com.github.ToshioCP.pr1", - G_APPLICATION_DEFAULT_FLAGS); - g_signal_connect(app, "activate", - G_CALLBACK(app_activate), NULL); - stat = g_application_run(G_APPLICATION(app), argc, argv); - g_object_unref(app); - - MPI_Abort(MPI_COMM_WORLD, 0); - MPI_Finalize(); - return stat; - } - - MPI_Recv(&mandelbrot, 1, planeView_t, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); - pixmap = malloc(sizeof(unsigned char) * 4 * mandelbrot.width * mandelbrot.height); - MPI_Win_create(pixmap, 3*mandelbrot.width*mandelbrot.height, 1, - MPI_INFO_NULL, MPI_COMM_WORLD, &window); - MPI_Win_fence(0, window); - - int h = mandelbrot.height; - int w = mandelbrot.width; - int p = procRank - 1, c = commSz - 1; - int mod = h % c, div = h / c; - int a = MIN(p, mod)*(div+1) + (MAX(p, mod)-mod)*div, - b = p < mod ? a + div + 1 : a + div; - - - double centerX = mandelbrot.centerX; - double centerY = mandelbrot.centerY; - double scale = mandelbrot.scale; - - for (int i = a; i < b; i++) { - double yRange = (double) i / h * 2.0 - 1.0; - double y = centerY+yRange*scale; - for (int j = 0; j < w; j++) { - double xRange = (double) j / w * 2.0 - 1.0; - double x = centerX+xRange*scale; - - int r, g, b; - color_from_iteration(&r, &g, &b, x, y); - pixmap[4*(i*w + j) + 2] = r; - pixmap[4*(i*w + j) + 1] = g; - pixmap[4*(i*w + j) + 0] = b; + if (argc == 1) { + thread_count = 4; + } else if (argc == 2 && !strcmp(argv[1], "-h")) { + print_usage(stdout); + exit(0); + } else if (argc == 2) { + char *endptr; + thread_count = strtol(argv[1], &endptr, 10); + if (argv[1][0] == '\0' || *endptr != '\0' || + thread_count < 1 || + thread_count > MAX_THREADS) { + // if argv[1] is not a valid decimal number, or + // is not in the range + fprintf(stderr, "Invalid number of threads, " + "terminating application!\n"); + print_usage(stderr); + exit(1); } + } else { + fprintf(stderr, "Wrong arguments, terminating application!\n"); + print_usage(stderr); + exit(1); } - MPI_Put(pixmap + 4*a*w, 4*(b-a)*w, MPI_UNSIGNED_CHAR, 0, 4*a*w, 4*(b-a)*w, MPI_UNSIGNED_CHAR, window); - MPI_Win_fence(0, window); + int i, count; + for (i = 0, count = 0; i < thread_count; i++) { + threads[count].index = count; + // we set this before the thread starts so it points to a valid + // integer, if the thread fails to start, it will simply be + // overwritten by the same value in the next run of the loop + if (pthread_create(&threads[count].id, NULL, &writer_thread, &threads[count].index)) { + fprintf(stderr, "Failed to create thread %d\n", i); + continue; + } + count++; + } + thread_count = count; + // Update thread count to show the number of threads actually created, + // not the number requested + if (thread_count == 0) { + fprintf(stderr, "Failed to start any threads, terminating " + "application\n"); + exit(1); + } - MPI_Finalize(); + // Main thread actually running GTK UI from here on out + + int stat = 0; + + GtkApplication *app; + app = gtk_application_new("cool.bonsai.mandelbrot-visualizer", + G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL); + stat = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + + // TODO: cancel all other threads here + return stat; }