Compare commits

...

13 commits

Author SHA1 Message Date
Petar Kapriš
8ad76abac9 Add queue_redraw_plane function
Create a function which queues the next draw, and add a call to it by
the final worker thread to finish drawing.
2025-10-13 22:19:01 +02:00
Petar Kapriš
f5be463c05 Simplify plane_resize function 2025-10-13 22:18:33 +02:00
Petar Kapriš
64f921eff7 Fix blit_plane
This edit will make sure blit_plane only acts if all workers have
completed their drawing, since it's not much use otherwise.
2025-10-13 22:18:33 +02:00
Petar Kapriš
99fc7ce405 Rename draw_plane to blit_plane 2025-10-13 22:18:33 +02:00
Petar Kapriš
96087d17a1 Fix cairo surface flushing and marking in create_surface 2025-10-13 22:18:33 +02:00
Petar Kapriš
94176c8d7c Move threads array and the cairo surface 2025-10-13 22:18:33 +02:00
Petar Kapriš
7f73925769 Mark cairo surface as dirty from final worker thread 2025-10-13 22:17:48 +02:00
Petar Kapriš
270c00cefc Make polling by workers rarer
Previously workers would poll for the availability of the drawing board
after every drawn pixel. Now they do it after every line, which was
originally the plan.
2025-10-13 21:01:47 +02:00
Petar Kapriš
9b19884286 Clean up compiler warnings 2025-10-13 16:12:01 +02:00
Petar Kapriš
ea699e5c48 Reorganize functions in code 2025-10-08 11:31:25 +02:00
Petar Kapriš
4b150c8097 Modify argument to worker thread functions
Instead of only supplying the index, the pointer to the threadInfo
structure is passed.
2025-10-08 10:29:56 +02:00
Petar Kapriš
ba685cc0ca Detach worker threads upon creation 2025-10-08 10:29:49 +02:00
Petar Kapriš
2a3fee2c3f Rewrite the program to use POSIX threads
This is work in progress, there might still be some concurrency issues
to be resolved here.
2025-10-07 13:26:39 +02:00
3 changed files with 339 additions and 167 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
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.

494
visor.c
View file

@ -1,30 +1,172 @@
#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;
/******* Program data structures *******/
int32_t thread_count;
struct threadInfo {
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
};
/*
* 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;
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
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};
/******* Worker thread code *******/
void draw_mandelbrot(struct threadInfo *info);
void color_from_iteration(int *r, int *g, int *b, double x0, double y0);
void color_lookup(int *r, int *g, int *b, double mu);
// This function is from the main thread, but it needs to be referenced by
// worker code
gboolean queue_redraw_plane(void *arg);
void *writer_thread(void *arg)
{
struct threadInfo *info = arg;
while (true) {
pthread_mutex_lock(&pixmapMutex);
while (!pixmapAvailable || info->complete) {
pthread_cond_wait(&pixmapCond, &pixmapMutex);
}
pthread_mutex_unlock(&pixmapMutex);
info->drawing = true;
draw_mandelbrot(info);
info->drawing = false;
}
}
void draw_mandelbrot(struct threadInfo *info)
{
int h = mandelbrot.height;
int w = mandelbrot.width;
int p = info->index, 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;
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);
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;
}
}
pthread_mutex_lock(&pixmapMutex);
info->complete = true;
bool allWritersComplete = true;
for (int32_t i = 0; i < thread_count; i++) {
if (!threads[i].complete) {
allWritersComplete = false;
}
}
if (allWritersComplete) {
cairo_surface_mark_dirty(surface);
g_main_context_invoke(NULL, queue_redraw_plane, NULL);
}
pthread_mutex_unlock(&pixmapMutex);
}
#define MAX_ITERATION 1000
#define ESC_RAD 20.0
int color_lookup(int *r, int *g, int *b, double mu)
void color_from_iteration(int *r, int *g, int *b, double x0, double y0)
{
double x = 0;
double y = 0;
int iteration = 0;
while (x*x + y*y <= ESC_RAD*ESC_RAD && iteration < MAX_ITERATION) {
double xtmp = xtmp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtmp;
iteration++;
}
// at this point iteration is the number of iterations required for the value to
// escape. X and Y contain the first escaped value
double mu = (iteration + 1 - log(log(sqrt(x*x+y*y)))/log(2));
if (mu < 0)
mu = 0;
if (mu > MAX_ITERATION)
mu = MAX_ITERATION;
color_lookup(r, g, b, mu);
}
void color_lookup(int *r, int *g, int *b, double mu)
{
static const int table16[16][3] = {
{ 66, 30, 15 },
@ -55,98 +197,100 @@ int color_lookup(int *r, int *g, int *b, double mu)
*r = *g = *b = 0;
}
}
void color_from_iteration(int *r, int *g, int *b, double x0, double y0)
{
double x = 0;
double y = 0;
int iteration = 0;
while (x*x + y*y <= ESC_RAD*ESC_RAD && iteration < MAX_ITERATION) {
double xtmp = xtmp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtmp;
iteration++;
}
// at this point iteration is the number of iterations required for the value to
// escape. X and Y contain the first escaped value
double mu = (iteration + 1 - log(log(sqrt(x*x+y*y)))/log(2));
if (mu < 0)
mu = 0;
if (mu > MAX_ITERATION)
mu = MAX_ITERATION;
color_lookup(r, g, b, mu);
#undef MAX_ITERATION
}
void draw_mandelbrot(unsigned char* data, int height, int width, double planeCenterX, double planeCenterY, double scale)
/******* Main thread code *******/
void print_usage(FILE *stream);
static void app_activate(GApplication *app);
void blit_plane(GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer data);
void plane_resize(GtkWidget *widget);
void create_surface(GtkWidget *widget);
int main(int argc, char **argv)
{
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 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;
pthread_mutex_init(&pixmapMutex, NULL);
pthread_cond_init(&pixmapCond, NULL);
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);
}
}
void create_surface(GtkWidget *widget)
{
cairo_t *cr;
if (surface)
cairo_surface_destroy(surface);
surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24,
gtk_widget_get_width(widget),
gtk_widget_get_height(widget));
cr = cairo_create(surface);
cairo_surface_flush(surface);
int h = cairo_image_surface_get_height(surface);
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);
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
pthread_t id;
if (pthread_create(&id, NULL, &writer_thread, &threads[count])) {
pthread_detach(id);
fprintf(stderr, "Failed to create thread %d\n", i);
continue;
}
count++;
}
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);
thread_count = count;
// Update thread count to show the number of threads actually created,
// not the number requested
unsigned char *data = cairo_image_surface_get_data(surface);
// draw_mandelbrot(data, h, w, mandelbrot.centerX, mandelbrot.centerY, mandelbrot.scale);
if (thread_count == 0) {
fprintf(stderr, "Failed to start any threads, terminating "
"application\n");
exit(1);
}
MPI_Win_fence(0, window);
memcpy(data, pixmap, 4*h*w);
cairo_surface_mark_dirty(surface);
// cairo_paint(cr);
// Main thread actually running GTK UI from here on out
cairo_destroy(cr);
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);
return stat;
}
void plane_resize(GtkWidget *widget, int width, int height)
void print_usage(FILE *stream)
{
if (!surface)
create_surface(widget);
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);
}
void draw_plane(GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer data)
{
cairo_set_source_surface(cr, surface, 0, 0);
cairo_paint(cr);
}
GtkDrawingArea *da;
static void app_activate(GApplication *app)
{
@ -155,7 +299,6 @@ static void app_activate(GApplication *app)
GtkWidget *btn2;
GtkWidget *bigBox;
GtkWidget *vertBox;
GtkDrawingArea *da;
win = gtk_application_window_new(GTK_APPLICATION(app));
gtk_window_set_title(GTK_WINDOW(win), "Mandelbrot visualiser");
@ -181,13 +324,14 @@ static void app_activate(GApplication *app)
bigBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
// global to enable other functions to call for a redraw
da = g_object_new(GTK_TYPE_DRAWING_AREA,
"accessible-role", GTK_ACCESSIBLE_ROLE_IMG,
NULL);
gtk_widget_set_hexpand(GTK_WIDGET(da), true);
gtk_drawing_area_set_content_width(GTK_DRAWING_AREA(da), 300);
gtk_drawing_area_set_content_height(GTK_DRAWING_AREA(da), 200);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(da), draw_plane, NULL, NULL);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(da), blit_plane, NULL, NULL);
g_signal_connect(da, "resize", G_CALLBACK(plane_resize), NULL);
// TODO: implement later drag = gtk_gesture_drag_new();
@ -199,81 +343,107 @@ static void app_activate(GApplication *app)
gtk_window_present(GTK_WINDOW(win));
}
int main(int argc, char **argv)
gboolean queue_redraw_plane(void *arg)
{
MPI_Init(NULL, 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;
(void) arg;
bool allWritersHadCompleted = true;
for (int32_t i = 0; i < thread_count; i++) {
if (!threads[i].complete) {
allWritersHadCompleted = false;
break;
}
}
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);
if (!allWritersHadCompleted) {
return G_SOURCE_REMOVE;
}
MPI_Finalize();
gtk_widget_queue_draw(GTK_WIDGET(da));
return G_SOURCE_REMOVE;
}
void blit_plane(GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer data)
{
(void) da;
(void) width;
(void) height;
(void) data;
bool allWritersHadCompleted = true;
for (int32_t i = 0; i < thread_count; i++) {
if (!threads[i].complete) {
allWritersHadCompleted = false;
break;
}
}
if (allWritersHadCompleted) {
cairo_set_source_surface(cr, surface, 0, 0);
cairo_paint(cr);
}
}
void plane_resize(GtkWidget *widget)
{
create_surface(widget);
}
void create_surface(GtkWidget *widget)
{
// Stop all current drawers
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;
break;
}
}
} while (!allWritersStopped);
// If all drawers hadn't completed, we need to call
// cairo_surface_mark_dirty in the main thread
bool allWritersHadCompleted = true;
for (int32_t i = 0; i < thread_count; i++) {
if (!threads[i].complete) {
allWritersHadCompleted = false;
break;
}
}
if (!allWritersHadCompleted && surface != NULL) {
cairo_surface_mark_dirty(surface);
}
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),
gtk_widget_get_height(widget));
int h = cairo_image_surface_get_height(surface);
int w = cairo_image_surface_get_width(surface);
mandelbrot.height = h;
mandelbrot.width = w;
// Mark the surface as ready to be drawn by memory access
// Then notify other threads to start drawing
// One of these threads will make the corresponding call using
// cairo_surface_mark_dirty, to notify they are all done drawing
cairo_surface_flush(surface);
pixmap = cairo_image_surface_get_data(surface);
// This lock is unnecessary since all other threads are stopped already
pthread_mutex_lock(&pixmapMutex);
pixmapAvailable = true;
pthread_mutex_unlock(&pixmapMutex);
pthread_cond_broadcast(&pixmapCond);
}