Rewrite the program to use POSIX threads

This is work in progress, there might still be some concurrency issues
to be resolved here.
This commit is contained in:
Petar Kapriš 2025-10-06 23:20:38 +01:00
parent efac829915
commit 2a3fee2c3f
3 changed files with 189 additions and 111 deletions

View file

@ -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}

View file

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

278
visor.c
View file

@ -1,30 +1,59 @@
#include <gtk/gtk.h>
#include <mpi.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <complex.h>
#include <pthread.h>
int procRank, commSz;
MPI_Datatype planeView_t;
#include <gtk/gtk.h>
#include <cairo.h>
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 (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);
}
if (procRank == 0) {
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);
}
// Main thread actually running GTK UI from here on out
int stat = 0;
GtkApplication *app;
app = gtk_application_new("com.github.ToshioCP.pr1",
app = gtk_application_new("cool.bonsai.mandelbrot-visualizer",
G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate",
G_CALLBACK(app_activate), NULL);
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();
// TODO: cancel all other threads here
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;
}
}
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);
MPI_Finalize();
}