This post explains how to generate a Mandelbrot set in parallel using Golang goroutines
.
Source code here: https://github.com/GiselaMD/parallel-mandelbrot-go
Mandelbrot Set
For those that are interest in what’s a Mandelbrot set, check https://en.wikipedia.org/wiki/Mandelbrot_set
The set formula is based on the position of x
and y
coordinates:
1x = x*x - y*y + a2y = 2*x*y + b
We also check if x*x + y*y > 4
to set the color.
But instead of going into math details, I would like to explain how we can use goroutines
to render that Mandelbrot set on the screen.
Getting into the code
This program is based on 4 main values that are going to impact the performance and resolution of the Mandelbrot set.
1maxIter = 10002samples = 20034numBlocks = 645numThreads = 16
maxIter
defines how many times the Mandelbrot formula will be calculated, resulting onx
andy
values.samples
is the number of interactions that generates RGB color values.numBlocks
is in how many pieces do you want to divide the image.numThreads
is the number ofgoroutines
that will be created.
To render the result on the screen I’ve used the Pixel library (github.com/faiface/pixel). On the main function we have something like this:
1func main() {2 pixelgl.Run(run)3}
Calling pixelgl.Run
puts PixelGL in control of the main function and there’s no way for us to run any code in the main function anymore. That’s why we need to pass another function inside pixelgl.Run
, which is the run
function.
1func run() {2 log.Println("Initial processing...")3 pixelCount = 04 img = image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))5 cfg := pixelgl.WindowConfig{6 Title: "Parallel Mandelbrot in Go",7 Bounds: pixel.R(0, 0, imgWidth, imgHeight),8 VSync: true,9 }1011 win, err := pixelgl.NewWindow(cfg)12 if err != nil {13 panic(err)14 }15 log.Println("Rendering...")16 start := time.Now()17 workBuffer := make(chan WorkItem, numBlocks)18 threadBuffer := make(chan bool, numThreads)19 drawBuffer := make(chan Pix, pixelTotal)2021 workBufferInit(workBuffer)22 go workersInit(drawBuffer, workBuffer, threadBuffer)23 go drawThread(drawBuffer, win)2425 for !win.Closed() {26 pic := pixel.PictureDataFromImage(img)27 sprite := pixel.NewSprite(pic, pic.Bounds())28 sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))29 win.Update()3031 if showProgress {32 fmt.Printf("\r%d/%d (%d%%)", pixelCount, pixelTotal, int(100*(float64(pixelCount)/float64(pixelTotal))))33 }3435 if pixelCount == pixelTotal {36 end := time.Now()37 fmt.Println("\nFinished with time = ", end.Sub(start))38 pixelCount++3940 if closeOnEnd {41 break42 }43 }44 }45}
The run
function is responsible for initialising and updating the window as well as creating the channels that will be used for our goroutines
.
The workBuffer
is the channel responsible for adding the information of each block (based on numBlocks
). Inside the workBufferInit
, the initial and final x
and y
values are sent to the channel so that each goroutines
that gets that piece of the image to work on can calculate the color without needing to know the global data, only what’s the range of x
and y
of that block.
1func workBufferInit(workBuffer chan WorkItem) {2 var sqrt = int(math.Sqrt(numBlocks))34 for i := sqrt - 1; i >= 0; i-- {5 for j := 0; j < sqrt; j++ {6 workBuffer <- WorkItem{7 initialX: i * (imgWidth / sqrt),8 finalX: (i + 1) * (imgWidth / sqrt),9 initialY: j * (imgHeight / sqrt),10 finalY: (j + 1) * (imgHeight / sqrt),11 }12 }13 }14}
The threadBuffer
is responsible for creating goroutines
based on the numThreads
and controlling when a goroutine
is done with its work so we can run another in its place. That logic inside workersInit
goroutine
.
1func workersInit(drawBuffer chan Pix, workBuffer chan WorkItem, threadBuffer chan bool) {2 for i := 1; i <= numThreads; i++ {3 threadBuffer <- true4 }56 for range threadBuffer {7 workItem := <-workBuffer89 go workerThread(workItem, drawBuffer, threadBuffer)10 }11}
For each workItem
that we receive from the workBuffer
(each block) we create a goroutine
called workerThread
to handle all the Mandelbrot set logic.
1func workerThread(workItem WorkItem, drawBuffer chan Pix, threadBuffer chan bool) {2 for x := workItem.initialX; x < workItem.finalX; x++ {3 for y := workItem.initialY; y < workItem.finalY; y++ {4 var colorR, colorG, colorB int5 for k := 0; k < samples; k++ {6 a := height*ratio*((float64(x)+RandFloat64())/float64(imgWidth)) + posX7 b := height*((float64(y)+RandFloat64())/float64(imgHeight)) + posY8 c := pixelColor(mandelbrotIteraction(a, b, maxIter))9 colorR += int(c.R)10 colorG += int(c.G)11 colorB += int(c.B)12 }13 var cr, cg, cb uint814 cr = uint8(float64(colorR) / float64(samples))15 cg = uint8(float64(colorG) / float64(samples))16 cb = uint8(float64(colorB) / float64(samples))1718 drawBuffer <- Pix{19 x, y, cr, cg, cb,20 }2122 }23 }24 threadBuffer <- true25}
1func mandelbrotIteraction(a, b float64, maxIter int) (float64, int) {2 var x, y, xx, yy, xy float6434 for i := 0; i < maxIter; i++ {5 xx, yy, xy = x*x, y*y, x*y6 if xx+yy > 4 {7 return xx + yy, i8 }9 // xn+1 = x^2 - y^2 + a10 x = xx - yy + a11 // yn+1 = 2xy + b12 y = 2*xy + b13 }1415 return xx + yy, maxIter16}1718func pixelColor(r float64, iter int) color.RGBA {19 insideSet := color.RGBA{R: 0, G: 0, B: 0, A: 255}2021 // check if it's inside the set22 if r > 4 {23 // return hslToRGB(float64(0.70)-float64(iter)/3500*r, 1, 0.5)24 return hslToRGB(float64(iter)/100*r, 1, 0.5)25 }2627 return insideSet28}
The drawBuffer
is the channel that receives the values from the goroutines
that are calculating the Mandelbrot set and once it receives data, the drawThread
goroutine
sets the pixel RGB value into the image and then the run
function updates the window.
1func drawThread(drawBuffer chan Pix, win *pixelgl.Window) {2 for i := range drawBuffer {3 img.SetRGBA(i.x, i.y, color.RGBA{R: i.cr, G: i.cg, B: i.cb, A: 255})4 pixelCount++5 }6}
We also have some utils functions for generating random data and converting hsl and hue to RGB:
1var randState = uint64(time.Now().UnixNano())23func RandUint64() uint64 {4 randState = ((randState ^ (randState << 13)) ^ (randState >> 7)) ^ (randState << 17)5 return randState6}78func RandFloat64() float64 {9 return float64(RandUint64() / 2) / (1 << 63)10}1112func hueToRGB(p, q, t float64) float64 {13 if t < 0 { t += 1 }14 if t > 1 { t -= 1 }15 switch {16 case t < 1.0 / 6.0:17 return p + (q - p) * 6 * t18 case t < 1.0 / 2.0:19 return q20 case t < 2.0 / 3.0:21 return p + (q - p) * (2.0 / 3.0 - t) * 622 default:23 return p24 }25}2627func hslToRGB(h, s, l float64) color.RGBA {28 var r, g, b float6429 if s == 0 {30 r, g, b = l, l, l31 } else {32 var q, p float6433 if l < 0.5 {34 q = l * (1 + s)35 } else {36 q = l + s - l * s37 }38 p = 2 * l - q39 r = hueToRGB(p, q, h + 1.0 / 3.0)40 g = hueToRGB(p, q, h)41 b = hueToRGB(p, q, h - 1.0 / 3.0)42 }43 return color.RGBA{ R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255), A: 255 }44}
Final result:
That’s it for today!
Hope you enjoy it 😊
Source code here: https://github.com/GiselaMD/parallel-mandelbrot-go