mightymandel v16

GPU-based Mandelbrot set explorer

mightymandel.c
Go to the documentation of this file.
1 // mightymandel -- GPU-based Mandelbrot Set explorer
2 // Copyright (C) 2012,2013,2014,2015 Claude Heiland-Allen
3 // License GPL3+ http://www.gnu.org/licenses/gpl.html
4 
5 #include <GL/glew.h>
6 #include <GLFW/glfw3.h>
7 
8 #include <math.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <mpfr.h>
13 
14 #include "startup.h"
15 #include "interact.h"
16 #include "render.h"
17 #include "parse.h"
18 #include "tiling.h"
19 #include "zoom.h"
20 #include "record.h"
21 #include "utility.h"
22 #include "filename.h"
23 #include "stopwatch.h"
24 #include "metadata.h"
25 #include "vram.h"
26 #include "poll.h"
27 #include "version.h"
28 #include "image.h"
29 
30 // initialize global state, see mightymandel.h for details
32 bool FP64 = true;
33 bool DE = true;
34 
35 // not all glfw3 callbacks include mouse coordinates, but we need them sometimes
36 // they are saved here by motion_handler() and read in the other *_handler()s
37 static double mouse_x = 0;
38 static double mouse_y = 0;
39 
40 // handle input from mouse motion
41 static void motion_handler(GLFWwindow *w, double x, double y) {
42  (void) w;
43  mouse_x = x;
44  mouse_y = y;
45 }
46 
47 // handle input from mouse button presses
48 static void button_handler(GLFWwindow *w, int button, int action, int mods) {
49  (void) w;
50  if (action == GLFW_PRESS) {
51  int shift = mods & GLFW_MOD_SHIFT;
52  int ctrl = mods & GLFW_MOD_CONTROL;
53  switch (button) {
54  case GLFW_MOUSE_BUTTON_LEFT: interact_mouse(b_left, mouse_x, mouse_y, shift, ctrl); break;
55  case GLFW_MOUSE_BUTTON_MIDDLE: interact_mouse(b_middle, mouse_x, mouse_y, shift, ctrl); break;
56  case GLFW_MOUSE_BUTTON_RIGHT: interact_mouse(b_right, mouse_x, mouse_y, shift, ctrl); break;
57  }
58  }
59 }
60 
61 // handle input from mouse scroll wheel actions
62 static void scroll_handler(GLFWwindow *w, double xoffset, double yoffset) {
63  (void) w;
64  (void) xoffset;
65  int direction = yoffset > 0 ? 1 : yoffset < 0 ? -1 : 0;
66  bool shift = glfwGetKey(w, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(w, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS;
67  bool ctrl = glfwGetKey(w, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(w, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
68  switch (direction) {
69  case 1: interact_mouse(b_up, mouse_x, mouse_y, shift, ctrl); break;
70  case -1: interact_mouse(b_down, mouse_x, mouse_y, shift, ctrl); break;
71  default: break;
72  }
73 }
74 
75 // handle input from keyboard key presses
76 static void key_press_handler(GLFWwindow *w, int key, int scancode, int action, int mods) {
77  (void) w;
78  (void) scancode;
79  bool shift = mods & GLFW_MOD_SHIFT;
80  bool ctrl = mods & GLFW_MOD_CONTROL;
81  if (action == GLFW_PRESS) {
82  switch (key) {
83  case GLFW_KEY_Q: case GLFW_KEY_ESCAPE: interact_keyboard(k_quit, shift, ctrl); break;
84  case GLFW_KEY_E: interact_keyboard(k_show_glitches, shift, ctrl); break;
85  case GLFW_KEY_PAGE_UP: interact_keyboard(k_zoom_in, shift, ctrl); break;
86  case GLFW_KEY_PAGE_DOWN: interact_keyboard(k_zoom_out, shift, ctrl); break;
87  case GLFW_KEY_S: interact_keyboard(k_save_screenshot, shift, ctrl); break;
88  case GLFW_KEY_0: interact_keyboard(k_weight_0, shift, ctrl); break;
89  case GLFW_KEY_1: interact_keyboard(k_weight_1, shift, ctrl); break;
90  case GLFW_KEY_2: interact_keyboard(k_weight_2, shift, ctrl); break;
91  case GLFW_KEY_3: interact_keyboard(k_weight_3, shift, ctrl); break;
92  case GLFW_KEY_4: interact_keyboard(k_weight_4, shift, ctrl); break;
93  case GLFW_KEY_5: interact_keyboard(k_weight_5, shift, ctrl); break;
94  case GLFW_KEY_6: interact_keyboard(k_weight_6, shift, ctrl); break;
95  case GLFW_KEY_7: interact_keyboard(k_weight_7, shift, ctrl); break;
96  case GLFW_KEY_8: interact_keyboard(k_weight_8, shift, ctrl); break;
97  case GLFW_KEY_9: interact_keyboard(k_weight_9, shift, ctrl); break;
98  case GLFW_KEY_UP: interact_keyboard(k_up, shift, ctrl); break;
99  case GLFW_KEY_DOWN: interact_keyboard(k_down, shift, ctrl); break;
100  case GLFW_KEY_LEFT: interact_keyboard(k_left, shift, ctrl); break;
101  case GLFW_KEY_RIGHT: interact_keyboard(k_right, shift, ctrl); break;
102  }
103  }
104 }
105 
106 // create a window with the desired OpenGL core profile version
107 static GLFWwindow* create_window(enum log_level_t level, int major, int minor, int width, int height) {
108  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, major);
109  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, minor);
110  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
111  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
112  glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
113  GLFWwindow *window = glfwCreateWindow(width, height, "mightymandel", 0, 0);
114  if (! window) {
115  log_message(level, "couldn't create window with OpenGL core %d.%d context\n", major, minor);
116  }
117  return window;
118 }
119 
120 char *collect_metadata_to_string(const char *prefix, struct render_options *render_options, struct tiling *tiling, struct zoom *zoom, int pass) {
121 #define ADD(key,format,...) do{ \
122  if (0 <= mpfr_asprintf(&s, format, __VA_ARGS__)) { \
123  metadata_update(meta, key, s); \
124  mpfr_free_str(s); \
125  } \
126 } while(0)
127  struct metadata *meta = metadata_new();
128  char *s;
129  metadata_update(meta, "software", "mightymandel");
130  metadata_update(meta, "version", mightymandel_version);
131  if (render_options->filename) {
132  s = strrchr(render_options->filename, '/');
133  metadata_update(meta, "filename", s ? s + 1 : render_options->filename);
134  }
135  ADD("view.real", "%Re", render_options->centerx);
136  ADD("view.imag", "%Re", render_options->centery);
137  ADD("view.radius", "%Re", render_options->radius);
138  ADD("view.aspect", "%f", render_options->width / (double) render_options->height);
139  ADD("image.width", "%d", render_options->width);
140  ADD("image.height", "%d", render_options->height);
141  ADD("image.pixels", "%d", render_options->width * render_options->height);
142  metadata_update(meta, "calc.escaperadius", "602.833931527923"); /* FIXME hardcoded */
143  double pxs = 2.0 * mpfr_get_d(render_options->radius, MPFR_RNDN) / render_options->height; /* FIXME precision */
144  ADD("calc.pixelspacing", "%.16e", pxs);
145  metadata_update(meta, "calc.fp", render_options->method == render_method_fp32 ? "fp32" : "fp64");
146  metadata_update(meta, "calc.perturb", render_options->method == render_method_fpxx ? "yes" : "no");
147  metadata_update(meta, "calc.perturb.approx", render_options->series_approx ? "yes" : "no");
148  ADD("calc.perturb.refcount", "%d", render_options->method == render_method_fpxx ? pass + 1 : 0);
149  ADD("calc.perturb.maxglitch","%.16e", render_options->max_glitch);
150  ADD("calc.perturb.maxblob","%d", render_options->max_blob);
151  ADD("calc.sharpness","%.16e", render_options->sharpness);
152  metadata_update(meta, "calc.distanceestimate", render_options->calculate_de ? "yes" : "no");
153  metadata_update(meta, "colour.distanceestimate", render_options->show_de ? "yes" : "no");
154  ADD("colour.weight","%.16e", render_options->weight);
155  metadata_update(meta, "colour.showglitches", render_options->show_glitches ? "yes" : "no");
156  metadata_update(meta, "zoom", zoom ? "yes" : "no");
157  if (zoom) {
158  ADD("zoom.real", "%Re", zoom->centerx);
159  ADD("zoom.imag", "%Re", zoom->centery);
160  ADD("zoom.radius", "%Re", zoom->radius);
161  ADD("zoom.frame", "%d", zoom->frame);
162  ADD("zoom.frames", "%d", zoom->zoom_frames);
163  }
164  metadata_update(meta, "tiling", tiling ? "yes" : "no");
165  if (tiling) {
166  ADD("tiling.real", "%Re", tiling->tiled_centerx);
167  ADD("tiling.imag", "%Re", tiling->tiled_centery);
168  ADD("tiling.radius", "%Re", tiling->tiled_radius);
169  ADD("tiling.cols", "%d", tiling->tiled_cols);
170  ADD("tiling.rows", "%d", tiling->tiled_rows);
171  ADD("tiling.col", "%d", tiling->col);
172  ADD("tiling.row", "%d", tiling->row);
173  }
174  double iterations = 0, exterior = 0, interior = 0, glitch = 0;
175  enum result_t result = image_result(render_options->width, render_options->height, &iterations, &exterior, &interior, &glitch);
176  ADD("stats.result", "%s", result_name[result]);
177  ADD("stats.maxiters", "%f", iterations);
178  ADD("stats.exterior", "%f", exterior);
179  ADD("stats.interior", "%f", interior);
180  ADD("stats.glitch", "%f", glitch);
181  int n = mpfr_snprintf(0, 0, "%smightymandel %Re + %Re i @ %Re\n", prefix, render_options->centerx, render_options->centery, render_options->radius);
182  int m = metadata_strlen(meta, prefix);
183  s = malloc(n + m + 1);
184  mpfr_snprintf(s, n + m + 1, "%smightymandel %Re + %Re i @ %Re\n", prefix, render_options->centerx, render_options->centery, render_options->radius);
185  metadata_string(meta, prefix, s + n, m + 1);
186  metadata_delete(meta);
187  return s;
188 #undef ADD
189 }
190 
191 // main program entrypoint
192 int main(int argc, char **argv) {
193  log_target = stderr;
194  srand(time(0));
195 
196  // set default options
197  struct options options;
198  memset(&options, 0, sizeof(struct options));
199  options.width = 1280;
200  options.height = 720;
201  options.win_width = 1280;
202  options.win_height = 720;
203  options.render_de = true;
204  options.series_approx = true;
205  options.log_level = LOG_NOTICE;
206  options.max_glitch = 0.02;
207  options.max_blob = 1;
208  options.sharpness = 0.01;
209 
210  // update options from arguments
211  if (! parse_command_line(&options, argc, argv)) {
212  log_message(LOG_FATAL, "couldn't parse command line\n");
213  return 1;
214  }
215  int sanity = options.interactive + options.oneshot + (options.tile || options.zoom);
216  if (sanity > 1) {
217  log_message(LOG_FATAL, "incompatible argument combination\n");
218  log_message(LOG_FATAL, "at most one of --interactive --one-shot (--tile || -zoom) can be used\n");
219  return 1;
220  } else if (sanity == 0) {
221  options.interactive = true;
222  }
223  if (options.tile && ! options.render_de) {
224  log_message(LOG_WARN, "tiling with --no-de is prone to seams at tile boundaries\n");
225  }
226  if (options.tile && options.render_de && ! options.show_glitches) {
227  log_message(LOG_WARN, "tiling with --no-glitch is prone to seams at tile boundaries\n");
228  }
229  if (options.size && ! options.geometry) {
230  options.win_width = options.width;
231  options.win_height = options.height;
232  }
233  if (! options.size && options.geometry) {
234  options.width = options.win_width;
235  options.height = options.win_height;
236  }
237  if (options.width != (options.width >> options.slice) << options.slice) {
238  log_message(LOG_FATAL, "width is not a multiple of slice factor %d\n", 1 << options.slice);
239  return 1;
240  }
241  if (options.height != (options.height >> options.slice) << options.slice) {
242  log_message(LOG_FATAL, "height is not a multiple of slice factor %d\n", 1 << options.slice);
243  return 1;
244  }
245 
246  // transfer command line options to renderer options
247  struct render_options render_options;
248  render_options_init(&render_options);
249  render_options.show_glitches = options.show_glitches;
250  render_options.calculate_de = options.render_de;
251  render_options.show_de = options.render_de;
252  render_options.series_approx = options.series_approx;
253  render_options.weight = options.weight;
254  render_options.max_glitch = options.max_glitch;
255  render_options.max_blob = options.max_blob;
256  render_options.sharpness = options.sharpness;
257  render_options.weight = options.weight;
258  render_options.width = options.width;
259  render_options.height = options.height;
260  render_options.win_width = options.win_width;
261  render_options.win_height = options.win_height;
262  render_options.slice = options.slice;
263  if (options.view_supplied) {
264  render_options_set_location(&render_options, options.centerx, options.centery, options.radius);
265  } else {
266  render_options.method = render_method_fp32;
267  mpfr_set_d(render_options.centerx, -0.75, MPFR_RNDN);
268  mpfr_set_d(render_options.centery, 0, MPFR_RNDN);
269  mpfr_set_d(render_options.radius, 2, MPFR_RNDN);
270  }
271  render_options.filename = options.file_to_load;
272 
273  // transfer options to legacy global state
274  DE = options.render_de;
275 
276  // print startup messages
277  if (options.version) {
278  log_target = stdout;
279  print_version();
280  return 0;
281  }
282  if (options.help) {
283  log_target = stdout;
284  print_usage(argv[0]);
285  return 0;
286  }
287  log_level = options.log_level;
288  print_banner();
289 
290  // load parameter file from the command line argument if any
291  if (options.file_to_load) {
292  mpfr_t cx, cy, cz;
293  mpfr_inits2(53, cx, cy, cz, (mpfr_ptr) 0);
294  if (! load_parameter_file(options.file_to_load, cx, cy, cz)) {
295  log_message(LOG_FATAL, "failed to load parameters: %s\n", options.file_to_load);
296  log_result(options.file_to_load, result_parse, render_method_fp32, 0, 0, 0, 0, 0);
297  return 1;
298  }
299  render_options_set_location(&render_options, cx, cy, cz);
300  mpfr_clears(cx, cy, cz, (mpfr_ptr) 0);
301  }
302 
303  // create OpenGL window context
304  if (! glfwInit()) {
305  log_message(LOG_FATAL, "couldn't initialize glfw\n");
306  return 1;
307  }
308  int major, minor;
309  GLFWwindow * window = create_window(LOG_WARN, major = 4, minor = 1, options.win_width, options.win_height);
310  if (! window) { window = create_window(LOG_ERROR, major = 3, minor = 3, options.win_width, options.win_height); }
311  FP64 = major >= 4;
312  if (! window) {
313  log_message(LOG_FATAL, "couldn't create window\n");
314  return 1;
315  }
316  if (! FP64) {
317  log_message(LOG_WARN, "no FP64 support available, zoom depth will be limited\n");
318  }
319  glfwMakeContextCurrent(window);
320  glewExperimental = GL_TRUE;
321  glewInit();
322  glGetError(); // discard common error from glew
323 
324  // check VRAM is enough, part 1...
325  int vram = vram_available();
326 
327  // set up renderer
328  struct render render;
329  render.begun = false;
330  render_begin(&render);
331  GLsizei bytes_allocated = 0;
332  render_reshape(&render, options.width, options.height, options.slice, &bytes_allocated);
333 
334  // check VRAM is enough, part 2...
335  int kb_allocated = (bytes_allocated + 1023) / 1024;
336  if (vram > 0) {
337  if (vram < kb_allocated) {
338  log_message(LOG_WARN, "not enough video memory available, performance may suffer\n");
339  log_message(LOG_WARN, "increasing --slice could reduce video memory consumption\n");
340  log_message(LOG_WARN, "current --slice value: %d\n", options.slice);
341  }
342  log_message(LOG_INFO, "available video memory: %10d kB\n", vram);
343  } else {
344  log_message(LOG_WARN, "available video memory: unknown\n");
345  }
346  log_message(LOG_INFO, "video memory we alloc: %10d kB\n", kb_allocated);
347 
348  // set up interaction
349  // interact uses internal global state because
350  // glfw3 doesn't provide a user pointer that it passes to callbacks
351  if (options.interactive) {
352  log_message(LOG_INFO, "interactive begin\n");
353  glfwSetKeyCallback(window, key_press_handler);
354  glfwSetMouseButtonCallback(window, button_handler);
355  glfwSetScrollCallback(window, scroll_handler);
356  glfwSetCursorPosCallback(window, motion_handler);
357  interact_begin(options.win_width, options.win_height, render_options.centerx, render_options.centery, render_options.radius, render_options.weight, render_options.show_glitches);
358  }
359 
360  // set up zooming
361  struct zoom zoom;
362  if (options.zoom) {
363  log_message(LOG_INFO, "zoom begin\n");
364  zoom_begin(&zoom, options.zoom_frames, render_options.centerx, render_options.centery);
365  zoom_next(&zoom);
366  render_options_set_location(&render_options, zoom.centerx, zoom.centery, zoom.radius);
367  }
368 
369  // set up tiling (do this after zoom so that zoom and tiling can be combined)
370  struct tiling tiling;
371  if (options.tile) {
372  log_message(LOG_INFO, "tile begin\n");
373  tiling_begin(&tiling, options.tile_width, options.tile_height, render_options.width, render_options.height, render_options.centerx, render_options.centery, render_options.radius);
374  if (tiling_next(&tiling)) {
375  render_options_set_location(&render_options, tiling.centerx, tiling.centery, tiling.radius);
376  } else {
377  glfwSetWindowShouldClose(window, GL_TRUE);
378  }
379  }
380 
381  // set up one-shot
382  if (options.oneshot) {
383  log_message(LOG_INFO, "one-shot begin\n");
384  }
385 
386  // set up recording
387  struct filename filename;
388  filename_begin(&filename);
389  struct record record;
390  record_begin(&record);
391 
392  // main loop
393  if (options.overhead) {
394  // nothing to do
395  } else if (options.interactive) {
396  struct poll *poll = poll_new(window, options.interactive, &render_options, 0.01, 0.1, &filename, &record);
397  // state for location printing
398  struct render_options last_render_options;
399  render_options_init(&last_render_options);
400  render_options_copy(&last_render_options, &render_options);
401  bool first = true;
402  // loop until quit
403  while (! glfwWindowShouldClose(window)) {
404  // print location info if changed
405  if (first || ! mpfr_equal_p(last_render_options.centerx, render_options.centerx)) {
406  log_message(LOG_NOTICE, "view.real %Re\n", render_options.centerx);
407  }
408  if (first || ! mpfr_equal_p(last_render_options.centery, render_options.centery)) {
409  log_message(LOG_NOTICE, "view.imag %Re\n", render_options.centery);
410  }
411  if (first || ! mpfr_equal_p(last_render_options.radius, render_options.radius)) {
412  log_message(LOG_NOTICE, "view.radius %Re\n", render_options.radius);
413  }
414  if (first || last_render_options.method != render_options.method) {
415  log_message(LOG_NOTICE, "calc.fp %s\n", render_options.method == render_method_fp32 ? "fp32" : "fp64");
416  log_message(LOG_NOTICE, "calc.perturb %s\n", render_options.method == render_method_fpxx ? "yes" : "no");
417  }
418  first = false;
419  render_options_copy(&last_render_options, &render_options);
420  // render
421  enum render_result result = render_do(&render, &render_options, poll);
422  // idle when completed
423  if (! glfwWindowShouldClose(window) && result == render_complete) {
424  while (poll_continue == poll_ui(poll, true)) {
425  // nothing to do
426  }
427  }
428  }
429  poll_delete(poll);
430  } else {
431  struct poll *poll = poll_new(window, options.interactive, &render_options, 0.1, 1.0, &filename, &record);
432  int frames = options.zoom ? options.zoom_frames : 1;
433  int cols = options.tile ? options.tile_width : 1;
434  int rows = options.tile ? options.tile_height : 1;
435  double timeout = options.timeout > 0.0 ? options.timeout : 1.0 / 0.0;
436  struct stopwatch *tile_time = stopwatch_new();
437  for (int frame = 0; ! glfwWindowShouldClose(window) && frame < frames; ++frame) {
438  if (options.zoom) {
439  log_message(LOG_NOTICE, "zoom %4d/%d\n", frame + 1, frames);
440  }
441  for (int row = 0; ! glfwWindowShouldClose(window) && row < rows; ++row) {
442  for (int col = 0; ! glfwWindowShouldClose(window) && col < cols; ++col) {
443  if (options.tile) {
444  log_message(LOG_NOTICE, "tile %4d/%d\n", col + row * cols + 1, rows * cols);
445  }
446  stopwatch_reset(tile_time);
447  poll_set_timeout(poll, timeout);
448  enum render_result result = render_do(&render, &render_options, poll);
449  double tile_took = stopwatch_elapsed(tile_time);
450  timeout = fmax(timeout, 1.1 * tile_took); // FIXME hardcoded factor
451  if (! glfwWindowShouldClose(window) && result == render_complete) {
452  char *comment = collect_metadata_to_string("# ", &render_options, options.tile ? &tiling : 0, options.zoom ? &zoom : 0, render_options.method == render_method_fpxx ? render.pass + 1 : 0);
453  char *name = filename_name(&filename, "ppm", frame, col, row);
454  record_do(&record, name, render_options.width, render_options.height, comment);
455  free(name);
456  free(comment);
457  if (options.tile) {
458  tiling_next(&tiling);
459  render_options_set_location(&render_options, tiling.centerx, tiling.centery, tiling.radius);
460  }
461  }
462  }
463  }
464  if (! glfwWindowShouldClose(window) && options.zoom) {
465  zoom_next(&zoom);
466  render_options_set_location(&render_options, zoom.centerx, zoom.centery, zoom.radius);
467  }
468  if (! glfwWindowShouldClose(window) && options.tile) {
469  tiling_end(&tiling);
470  tiling_begin(&tiling, options.tile_width, options.tile_height, render_options.width, render_options.height, render_options.centerx, render_options.centery, render_options.radius);
471  tiling_next(&tiling);
472  render_options_set_location(&render_options, tiling.centerx, tiling.centery, tiling.radius);
473  }
474  }
475  }
476 
477  // cleanup and exit
478  if (options.interactive) {
479  interact_end();
480  }
481  if (options.tile) {
482  tiling_end(&tiling);
483  }
484  if (options.zoom) {
485  zoom_end(&zoom);
486  }
487  render_end(&render);
488  record_end(&record);
489  glfwTerminate();
490  return 0;
491 }