Other Mandelbrot set renderer written in pure Makefile.


This is a Mandelbrot set renderer, written in pure BSD Makefiles, with no calls to ANY external binaries.
This project started after I was reading a bunch of the port system's Makefiles, and I realized that the language is actually computationally universal, so I had to write something to try it out, and here we are!

1783175204940.png

I was on the edge on putting this in this sub-forum or off-topics, I hope it is OK here.
 
This is very cool. Bookmarked to present next time someone says Make is too restrictive ;)

My general statement about make is that it can always do exactly 99% of the task required but no more. And that last 1% is a massive ugly pain in the butt. With your experience of this project, have you noticed similar?
 
Code:
root@Z600:~/code/bmake-extravaganza # cloc .
      30 text files.
      28 unique files.                             
      10 files ignored.

github.com/AlDanial/cloc v 2.08  T=0.04 s (720.5 files/s, 74369.4 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
make                            27            114            194           2473
Markdown                         1             13              0             96
-------------------------------------------------------------------------------
SUM:                            28            127            194           2569
-------------------------------------------------------------------------------

👏👍😎
 
This is very cool. Bookmarked to present next time someone says Make is too restrictive ;)

My general statement about make is that it can always do exactly 99% of the task required but no more. And that last 1% is a massive ugly pain in the butt. With your experience of this project, have you noticed similar?
This project was basically just delving in that 1% XD.
Floating point arithmetic, infinite loops, arbitrary calculations ...

But bmake also has some hidden, unique, solutions for some problems, I for one didn't know that you could print out a variable cleanly to stdout, mid-parsing, by mixing the -v flag and .MAKEFLAGS: special target, I learned that while doing this project.
The "dot-prefix" special targets and variables are definitely a great and IMO underrated tool.
 

To be frank, 900 lines of that is the reciprocal initiation table for division, 100~ lines for the multiplication table, and 400 lines for the addition and subtraction tables, so there would be about 1000 lines of actual logic code.
 
Actually, we are entering in the software implementation of floating point arithmetic, which is highly enthusiastic.

The question I ask myself is why don't use everywhere the same layout: number = sign.mantissa.10^-scale ? This will standardize the API and make precision explicit and easily tunable with the size of the mantissa. I thought using a fixed point would upgrade efficiency for hot Mandelbrot paths.
 
Actually, we are entering in the software implementation of floating point arithmetic, which is highly enthusiastic.

The question I ask myself is why don't use everywhere the same layout: number = sign.mantissa.10^-scale ? This will standardize the API and make precision explicit and easily tunable with the size of the mantissa. I thought using a fixed point would upgrade efficiency for hot Mandelbrot paths.

If I wanted to do something similar to a virtual ISA I would go with a IEEE 754 system, but that would be super slow and unnecessary for this project:
  1. Each multiplication/division would need another bit of decimal arithmetic for calculating the resulting exponents. While currently it is done with a simple dot shifting system.
  2. Adders would also need a way for scaling the numbers properly based on the decimal number of the exponent and they then needed to cut the result down to size. (super slow)
  3. You would have to create pre-filled lists, based on the chosen size, for the key .for loops going over the digits of the mantissa in different hot functions. That would also slow down those loops slightly, which would add up.
  4. You would have to make sure this system also works with bmake's arithmetic conditionals (which it doesn't) or you would also need to write your own comparison functions, which would also be super slow.
To see why 16 was chosen for the current number size run the code below:

Makefile:
a=98
b=99
.for i in 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
. info \# $(i)
. info $(a)
. info <
. info $(b)
. if $(a) < $(b)
.  info TRUE
. else
.  warning FALSE
. endif
a:=$(a:S/9/99/)
b:=$(b:S/9/99/)
. info $( :L)
.endfor
all: .PHONY
 
Nice!
I had a brief look at the implementation. I didn't expect to see floating point. Do you have some technical specs / benchmarks (accuracy, not performance) of the floating point math?

What kind of output did you pick? BMP? How do you write the output file?
The readme would suggest PPM. At which point you "just write human readable text to a file".
 
What kind of output did you pick? BMP? How do you write the output file?

Very cool project.
Thanks!

I chose the PPM format, since it is simple and text based.
I did have issues coming up with a clean way of outputting an image. Since I set my goal to use absolutely no external binaries (Not even recalling make itself), I had to come up with a way to make make itself write to either stdout or stderr, mid-parsing.
The debug directives (.info, .warning, .error) look like an attractive solution at first but they have a bunch of issues that make them not fit this case:
  1. They always prepend the output text, with line number and file name.
  2. They output to stderr.
Since the PPM format requires the magic 'P3' string to be the first thing on the first line of the image file, issue 1. alone made me cross out debug directives. On top of that, issue 2. meant that even if I could somehow solve issue 1., I then couldn't show a simple TUI and progress indicator, because all of that text would also get prepended at the start of stderr.
Next, for a short while I considred using a .PHONY target with PATH set to /nonexistant, and then just ploping the variable holding the image as an unmuted command; make would run the target, print the command line to stdout (which would actually be the image), and then fail running it as a command and exit.
It kinda worked, but this sloution is also cheating because make targets call the shell under the hood, and that would still be a single extarnal binary called, so I looked for yet another option:
I knew of -V and -vflags' behavior; when calling make you specify a variable name with them and they print them to stdout, no preemble, just its raw value. What I didn't know was that you could add them mid-parsing with the .MAKEFLAGES: special target.
By mixing these two you could print a variable clealy to stdout, with make itself.
Here is the print.mk file that does this.
So as pointed out in the readme, all you need to do is to run make and redirect it's stdout to your image file:
make > output.ppm

Binary output is also posible, the :tsc modifier accepts hex escape values, which creates a way for generating arbitury bytes, but the null byte (\x00) can never (to the extent of my knowlage) be printed by make itself (Null terminated strings...), so while a bmp output is possible, it would need a target with a bunch of calls to
printf(1) a call to shell and so on ... .
IDK I might add support for it just for giggles.
 
Very cool, again.

It is too bad that make doesn't have output to arbitrary file descriptors such as shell has:
ls 1>&3
and then redirect from the outside
./myscript 3> myfile

I missed this for some ports work long ago.
 
Nice!
I had a brief look at the implementation. I didn't expect to see floating point. Do you have some technical specs / benchmarks (accuracy, not performance) of the floating point math?


The readme would suggest PPM. At which point you "just write human readable text to a file".
Thanks!
The "floating point" implementation is weird, it had to work around make's limitations, be stored in strings and be somewhat optimized for rendering the set, and it is entirely in decimal, so here it is:
  • Numbers are not numbers, they are strings, unless put in a make conditional.

    Fundamentally make does not have a numerical variable type; everything is a string, we have to use string operations, regex's, LUTs, and other weird tactics to do basic arithmetic, only in select cases make converts a variable to an actual number under the hood, and that is in the arithmetic conditionals and in other fringe cases like selecting words with :[] and ...
    Since we really want to use these conditionals and not implement our own, the system should be compatible with what they can parse.

  • All numbers (sign excluded) have to be at maximum 16 chars wide.

    This is because, for some reason, make's arithmetic conditionals break when you have numbers above 17 digits. I chose 16 for being safe. Try the snippet here for seeing it in action.
    We do not need anything more than this for rendering the set with modest zoom levels (high zoom levels with this renderer could take ages, so I doubt anyone would hit them).

  • The radix point ('.') is also counted as a digit and included in the number string.

    This is the crux of the format, the radix point "floats", but it also takes a slot in the 16 available slots for a number. It works exactly as hand and paper numbers, so it is also super easy to read and debug.
    It is NOT a classical "mantissa * base ^ exponent" system, the radix literally floats in the available slots.
    As I explained above a IEEE 754 system is definitely possible, but it would be horribly slow esp. for a fractal renderer.

  • The radix point can be omitted in integers.

    There is no need for:
    123.0
    it can just be just given as:
    123

  • Trailing and leading zeros are non significant and can be omitted.

    .123
    123.
    Are valid numbers.
    Even:
    .
    is a valid number that expands to 0.0.
With all of the above in mind, the resulting system would have a range of:
Code:
Max: [+]9999999999999999
Min: [-].000000000000001
While it can not store super massive/small numbers like normal floats can, we do not need them for small zooms into the fractal, Since the Mandelbrot set is contained in a circle of radius 2, centered on zero in the imaginary-real plain, we spend most our time in that range, and we aim to detect points where the fractal's formula escapes to infinity, so being somewhat limited on the "positive exponent side" actually helps us spend less iterations on the points belonging to the set (i.e. the points painted pitch black).
The closest real-world analogue I could find for a similar system, was COBOL's PICture type, but it doesn't match this perfectly.
 
Very cool, again.

It is too bad that make doesn't have output to arbitrary file descriptors such as shell has:
ls 1>&3
and then redirect from the outside
./myscript 3> myfile

I missed this for some ports work long ago.
I haven't needed to do something like that but, perhaps a mixture of -dFfilename flag for debug outputs, fdescfs(4), and maybe doing some hacking with the .SHELL target might work. I'm not sure though.
 
Back
Top