Update: Version 3 is here.
My first version was an answer I provided to EBrown for his original post titled "Multithreaded Mandelbrot Generator". My answer had many good things in it, but I felt some of parts of the code were weak, particular around my efforts to apply some flexible scaling rather than using a constant of 2. This version addresses those weaknesses, tightens up some code, and simplified the multithreading even more.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Original CodeReview Question:
// https://codereview.stackexchange.com/questions/104171/multithreaded-mandelbrot-generator
// Interesting link about coloring:
// http://www.fractalforums.com/programming/newbie-how-to-map-colors-in-the-mandelbrot-set/
// Trusty Wikipedia:
// https://en.wikipedia.org/wiki/Mandelbrot_set
namespace Mandelbrot_Generator
{
public class MandelbrotGeneratorV2
{
// Readonly properties to be set in constructor
public int Width { get; }
public int Height { get; }
public short MaxIterations { get; }
public float ScaleFactor { get; }
private short[] _iterationsPerPixel = null;
private Point _center;
private SizeF _scaleSize;
private float _scaleSquared;
public MandelbrotGeneratorV2(int height, short maxIterations, float scaleFactor = 2.0F)
{
// Use some very basic level limit checking using some arbitrary (but practical) limits.
const int heightLow = 512;
const int heightHigh = 4096 * 2;
const short iterationLow = 100;
const short iterationHigh = 32000;
const float scaleLow = 1.0F;
const float scaleHigh = 8.0F;
CheckLimits(nameof(height), height, heightLow, heightHigh);
CheckLimits(nameof(maxIterations), maxIterations, iterationLow, iterationHigh);
CheckLimits(nameof(scaleFactor), scaleFactor, scaleLow, scaleHigh);
ScaleFactor = scaleFactor;
Width = (int)(scaleFactor * height);
Height = height;
MaxIterations = maxIterations;
_center = new Point(Width / 2, Height / 2);
// And we'll scale the size so the brot sits within region [-ScaleFactor,ScaleFactor],
_scaleSquared = ScaleFactor * ScaleFactor;
_scaleSize = new SizeF(_center.X / ScaleFactor, _center.Y);
}
private void CheckLimits(string name, double value, double inclusiveLow, double inclusiveHigh)
{
if (value < inclusiveLow || value > inclusiveHigh)
{
throw new ArgumentOutOfRangeException(name, $"Argument must be between {inclusiveLow} and {inclusiveHigh} inclusively.");
}
}
public void Generate()
{
_iterationsPerPixel = new short[Width * Height];
var sections = GetHoriztonalSections();
Parallel.ForEach(sections, section =>
{
var data = GenerateSection(section);
for (var y = section.Start.Y; y < section.End.Y; y++)
{
var brotOffset = y * Width;
var dataOffset = (y - section.Start.Y) * Width;
for (var x = 0; x < Width; x++)
{
_iterationsPerPixel[brotOffset + x] = data[dataOffset + x];
}
}
});
}
public void SaveImage(string filename) => SaveImage(filename, ImageFormat.Png);
public void SaveImage(string filename, ImageFormat imageFormat)
{
if (_iterationsPerPixel == null || _iterationsPerPixel.Length == 0)
{
throw new Exception("You must create the Mandelbrot data set before you can save the image to file.");
}
// Create our image.
using (Bitmap image = new Bitmap(Width, Height))
{
for (var y = 0; y < Height; y++)
{
var brotOffset = y * Width;
for (var x = 0; x < Width; x++)
{
image.SetPixel(x, y, LookupColor(_iterationsPerPixel[brotOffset + x]));
}
}
image.Save(filename, imageFormat);
}
}
// Coloring is probably has the greatest potential for further improvements.
// This is just one attempt that suffices for now.
private Color LookupColor(short iterations)
{
if (iterations >= MaxIterations)
{
return Color.Black;
}
if (iterations < 64)
{
return Color.FromArgb(255, iterations * 2, 0, 0);
}
if (iterations < 128)
{
return Color.FromArgb(255, (((iterations - 64) * 128) / 126) + 128, 0, 0);
}
if (iterations < 256)
{
return Color.FromArgb(255, (((iterations - 128) * 62) / 127) + 193, 0, 0);
}
if (iterations < 512)
{
return Color.FromArgb(255, 255, (((iterations - 256) * 62) / 255) + 1, 0);
}
if (iterations < 1024)
{
return Color.FromArgb(255, 255, (((iterations - 512) * 63) / 511) + 64, 0);
}
if (iterations < 2048)
{
return Color.FromArgb(255, 255, (((iterations - 1024) * 63) / 1023) + 128, 0);
}
if (iterations < 4096)
{
return Color.FromArgb(255, 255, (((iterations - 2048) * 63) / 2047) + 192, 0);
}
return Color.FromArgb(255, 255, 255, 0);
}
private struct Section
{
public Point Start { get; }
public Point End { get; }
// The way I create sections, End.Y will always be greater than Start.Y
// but the math nerd in me insists on using Math.Abs() anyway.
public int Height => Math.Abs(End.Y - Start.Y);
public int Width => Math.Abs(End.X - Start.X);
public Section(Point start, Point end)
{
Start = start;
End = end;
}
}
private Section[] GetHoriztonalSections()
{
var sections = new Section[2 * Environment.ProcessorCount];
var heightPerSection = Height / sections.Length;
if (Height % sections.Length > 0) { heightPerSection++; }
for (var i = 0; i < sections.Length - 1; i++)
{
var startY = heightPerSection * i;
sections[i] = new Section(new Point(0, startY), new Point(Width, startY + heightPerSection));
}
// SPECIAL TREATMENT FOR LAST SECTION:
// The width is the same per section, namely the image's Width,
// but the very last section's height could be different since
// it's upper rightmost point really should be clamped to the image's boundaries.
{
var lastIndex = sections.Length - 1;
var startY = heightPerSection * lastIndex ;
sections[lastIndex] = new Section(new Point(0, startY), new Point(Width, Height));
}
return sections;
}
private short[] GenerateSection(Section section)
{
// The sectionWidth is the same value as Width but for some odd reason
// using Width is noticeably faster on my 8-core PC. This is true even
// if I create a local copy such as:
// var sectionWidth = section.Width;
var data = new short[section.Height * Width];
for (var y = section.Start.Y; y < section.End.Y; y++)
{
var indexOffset = (y - section.Start.Y) * Width;
var anchorY = (y - _center.Y) / _scaleSize.Height;
for (var x = section.Start.X; x < section.End.X; x++)
{
// The formula for a mandelbrot is z = z^2 + c, basically. We must relate that in code.
var anchorX = (x - _center.X) / _scaleSize.Width;
short iteration;
float xTemp = 0;
float yTemp = 0;
float xSquared = 0;
float ySquared = 0;
for (iteration = 0; iteration < MaxIterations; iteration++)
{
if (xSquared + ySquared >= _scaleSquared) { break; }
// Important for yTemp to be calculated BEFORE xTemp
// since yTemp depends on older value of xTemp.
yTemp = 2 * xTemp * yTemp + anchorY;
xTemp = xSquared - ySquared + anchorX;
xSquared = xTemp * xTemp;
ySquared = yTemp * yTemp;
}
data[indexOffset + x] = iteration;
}
}
return data;
}
}
}
Differences in Versions
The constructor no longer requires separate Width
and Height
arguments. Instead a Height
is used along with a ScaleFactor
, which defaults to 2.0F
. The Width
is then calculated inside the constructor.
After browsing some forums of other Mandelbrot fans, who make reference to a book, I changed some names to align with their usage, e.g. _iterationsPerPixel
or MaxIterations
.
Only the bare minimum properties are now public
. Others were switched to fields and made private
; such private fields had their names modified to begin with an underscore.
The GenerateSection
was modified slightly. The old anchor
point was split into new floats named anchorX
and anchorY
. Slight improvement because anchorY
is calculated once before entering the X loop, rather than repeatedly inside the X loop.
The older version used EBrown’s original NumberOfSections
and NumberOfCores
. Neither of these affect the quality of the computed values for the pixels. Rather both were used as division of work across multiple threads. While I had simplified the multiple threads in my first answer (thanks to TPL), I realized I can simplify it even further. This version does not expose NumberOfSections
or NumberOfCores
as public
as I feel they really are implementation details that someone using the class should not be concerned with.
I added different coloring. Not necessarily better. Just different. Ironically my whole interest in picking up this subject was to eventually try some cool coloring effects. But before I can get to that stage, I want to make sure the rest of the class is solid.
Avoiding Magic Numbers:
I paid attention to avoiding magic numbers. Some methods have constants declared to avoid using magic numbers. However, in GetHoriztonalSections
, it could be said that I am using a magic number with:
var sections = new Section[2 * Environment.ProcessorCount];
I can’t use 2 * Environment.ProcessorCount
as a constant (since it’s not). I can’t declare the expression as readonly
inside the one-and-only method that uses it. I could declare it as readonly
to the entire class, but that increases its scope far beyond the one method that uses it.
And I found this to be less readable and more code than was needed:
const int factor = 2;
var numberOfSections = factor * Environment.ProcessorCount;
var sections = new Section[numberOfSections];
And I don't think it clarified my intent any more than the one line I settled upon.
Example on Usage:
private static void RickVersion2()
{
Console.WriteLine("\nRICK's 2ND VERSION:\n");
var brot = new MandelbrotGeneratorV2(height: 2048, maxIterations: 1000);
Console.WriteLine($"Creating Mandelbrot image of size ({brot.Width},{brot.Height}), max iteration of {brot.MaxIterations}, and Width:Height scale of {brot.ScaleFactor}.");
Console.WriteLine("\n\tGenerating Mandelbrot set ...");
var sw = Stopwatch.StartNew();
brot.Generate();
sw.Stop();
Console.WriteLine($"\tMandelbrot generation took {sw.ElapsedMilliseconds}ms.");
Console.WriteLine("\n\tSaving image to file ...");
sw.Restart();
brot.SaveImage("test3.png");
sw.Stop();
Console.WriteLine($"\tImage save took {sw.ElapsedMilliseconds}ms.");
}
Here's a shot of the console output:
And here's a snipped section of the graphic:
While coloring is a future interest to me, I have no interest in reviews of the code I use for coloring. However, I am quite interested in discussions on possible coloring techniques.
I have no interest in making the Generate
method cancellable (yet). Yes, I could pass a cancellation token in somehow. I don’t rule that out for a future version. I’ve even pondered having a Status
property based on an enum
like { NotGenerated, Generating, Generated }
. But that's for another day.