mandelbrot-visualiser/visor.c
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

388 lines
10 KiB
C

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <complex.h>
#include <pthread.h>
#include <gtk/gtk.h>
#include <cairo.h>
#define DEFAULT_THREADS 4
#define MAX_THREADS 32
#define STR(x) #x
/******* 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
};
struct threadInfo threads[MAX_THREADS]; // this will store an array with status info for all the threads
cairo_surface_t *surface = NULL;
/*
* 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};
/******* 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);
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;
}
}
info->complete = true;
}
#define MAX_ITERATION 1000
#define ESC_RAD 20.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);
}
void color_lookup(int *r, int *g, int *b, double mu)
{
static const int table16[16][3] = {
{ 66, 30, 15 },
{ 25, 7, 26 },
{ 9, 1, 47 },
{ 4, 4, 73 },
{ 0, 7, 100 },
{ 12, 44, 138 },
{ 24, 82, 177 },
{ 57, 125, 209 },
{ 134, 181, 229 },
{ 211, 236, 248 },
{ 241, 233, 191 },
{ 248, 201, 95 },
{ 255, 170, 0 },
{ 204, 128, 0 },
{ 153, 87, 0 },
{ 106, 52, 3 }
};
// *r = 0; // (1000.0-i)/1000.0*256.0;
// *g = 0; // (1000.0-i)/1000.0*256.0;
// *b = ((double) MAX_ITERATION - i - 1) / MAX_ITERATION * 255.0 - log(log(2)) / log(2); // (i/MAX_ITERATION)
int mod = (int) mu % 16;
*r = table16[mod][0];
*g = table16[mod][1];
*b = table16[mod][2];
if (mu == 0 || mu >= MAX_ITERATION - 50) {
*r = *g = *b = 0;
}
}
#undef MAX_ITERATION
/******* Main thread code *******/
void print_usage(FILE *stream);
static void app_activate(GApplication *app);
void draw_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)
{
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);
}
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++;
}
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("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 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);
}
static void app_activate(GApplication *app)
{
GtkWidget *win;
GtkWidget *btn1;
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");
gtk_window_set_default_size(GTK_WINDOW(win), 800, 600);
btn1 = gtk_button_new_with_label("A");
gtk_widget_set_size_request(GTK_WIDGET(btn1), 20, 20);
gtk_widget_set_margin_bottom(GTK_WIDGET(btn1), 5);
gtk_widget_set_margin_top(GTK_WIDGET(btn1), 5);
gtk_widget_set_margin_start(GTK_WIDGET(btn1), 5);
gtk_widget_set_margin_end(GTK_WIDGET(btn1), 5);
btn2 = gtk_button_new_with_label("B");
gtk_widget_set_size_request(GTK_WIDGET(btn2), 20, 20);
gtk_widget_set_margin_bottom(GTK_WIDGET(btn2), 5);
gtk_widget_set_margin_top(GTK_WIDGET(btn2), 5);
gtk_widget_set_margin_start(GTK_WIDGET(btn2), 5);
gtk_widget_set_margin_end(GTK_WIDGET(btn2), 5);
vertBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_valign(GTK_WIDGET(vertBox), GTK_ALIGN_START);
gtk_box_append(GTK_BOX(vertBox), btn1);
gtk_box_append(GTK_BOX(vertBox), btn2);
bigBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
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);
g_signal_connect(da, "resize", G_CALLBACK(plane_resize), NULL);
// TODO: implement later drag = gtk_gesture_drag_new();
gtk_box_append(GTK_BOX(bigBox), GTK_WIDGET(da));
gtk_box_append(GTK_BOX(bigBox), vertBox);
gtk_window_set_child(GTK_WINDOW(win), bigBox);
gtk_window_present(GTK_WINDOW(win));
}
void draw_plane(GtkDrawingArea *da, cairo_t *cr, int width, int height, gpointer data)
{
(void) da;
(void) width;
(void) height;
(void) data;
cairo_set_source_surface(cr, surface, 0, 0);
cairo_paint(cr);
}
void plane_resize(GtkWidget *widget)
{
if (!surface) {
create_surface(widget);
}
}
void create_surface(GtkWidget *widget)
{
cairo_t *cr;
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),
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;
pixmap = cairo_image_surface_get_data(surface);
cairo_surface_mark_dirty(surface);
pthread_mutex_lock(&pixmapMutex);
pixmapAvailable = true;
pthread_mutex_unlock(&pixmapMutex);
pthread_cond_broadcast(&pixmapCond);
cairo_destroy(cr);
}