# Mandelbrot set using RISC-V assembler

#1

In an earlier post I described a uLisp program to plot the Mandelbrot set. I decided to try speeding up the program by writing the inner loop in RISC-V machine code using the RISC-V assembler.

### Mandelbrot set in uLisp

Here’s the Mandelbrot set program running on a MAiX One Dock board:

Here’s the original program in Lisp, slightly modified to make the inner loop a separate function, iterate:

``````(defun mandelbrot (x0 y0 scale)
(set-rotation 2)
(fill-screen)
(dotimes (y 240)
(let ((b (+ (/ (- y 120) 120 scale) y0)))
(dotimes (x 320)
(let* ((a (+ (/ (- x 160) 120 scale) x0))
(c (iterate a b)))
(draw-pixel x y (if (plusp c) (hsv (* 359 (/ c 80)) 1 1) 0)))))))
``````

Here’s the iterate function:

``````(defun iterate (a0 b0)
(let ((c 80) (a a0) (b b0) a2)
(loop
(setq a2 (+ (- (* a a) (* b b)) a0))
(setq b (+ (* 2 a b) b0))
(setq a a2)
(decf c)
(when (or (> (+ (* a a) (* b b)) 4) (zerop c)) (return c)))))
``````

These functions also call rgb and hsv to choose the colours for the contours:

``````(defun rgb (r g b)
(logior (ash (logand r #xf8) 8) (ash (logand g #xfc) 3) (ash b -3)))

(defun hsv (h s v)
(let* ((chroma (* v s))
(x (* chroma (- 1 (abs (- (mod (/ h 60) 2) 1)))))
(m (- v chroma))
(i (truncate h 60))
(params (list chroma x 0 0 x chroma))
(r (+ m (nth i params)))
(g (+ m (nth (mod (+ i 4) 6) params)))
(b (+ m (nth (mod (+ i 2) 6) params))))
(rgb (round (* r 255)) (round (* g 255)) (round (* b 255)))))
``````

To plot the whole Mandelbrot set call:

``````(mandelbrot -0.5 0 1)
``````

The section I displayed in the above photograph is obtained with:

``````(mandelbrot -0.53 -0.61 11)
``````

For convenience, here’s a function go that plots this and returns the time taken:

``````(defun go () (for-millis () (mandelbrot -0.53 -0.61 11)))
``````

On a MAiX board running at 400 MHz the uLisp version takes 230 seconds.

### Converting the iterate function to assembler

To speed up the plotting I rewrote the iterate function in RISC-V assembler, using the assembler written in uLisp.

First load the assembler code from here: RISC-V assembler in uLisp.

Fortunately the K210 processor used on the MAiX boards includes floating-point instructions, so we can use these to perform the arithmetic. I didn’t include support for these in the original assembler so they need to be added from here: RISC-V assembler floating-point extensions.

Here’s the assembler version of iterate. It’s pretty much a direct conversion of the uLisp version above:

``````(defcode iterate (a b)
(\$flw 'fa0 8 '(a0)) ;a0
(\$flw 'fa1 8 '(a1)) ;b0
(\$fmv.s 'ft0 'fa0) ;a
(\$fmv.s 'ft1 'fa1) ;b
(\$li 'a4 2)
(\$fcvt.s.w 'ft5 'a4) ; ft5=2
(\$li 'a0 80)
again
(\$fmul.s 'ft2 'ft0 'ft0)
(\$fmul.s 'ft3 'ft1 'ft1)
(\$fsub.s 'ft4 'ft2 'ft3)
(\$fmul.s 'ft6 'ft0 'ft1)
(\$fmul.s 'ft7 'ft6 'ft5)
(\$fmv.s 'ft0 'ft4) ;a
(\$beqz 'a0 ret)
(\$fmul.s 'ft6 'ft0 'ft0)
(\$fmul.s 'ft7 'ft1 'ft1)
(\$fcvt.w.s 'a3 'ft7)
(\$blez 'a3 again)
ret
(\$ret))
``````

If you assemble this code it will replace the Lisp version, and you can then run (go) again to see the speed improvement.

The version with a machine-code version of iterate takes 43 seconds, over five times faster.

#2

Another optimisation is to precompute the colour numbers for each of the 80 possible contour values; I don’t know why I didn’t think of this before.

Define the list of values, *contours*, as follows:

``````(defvar *contours*
(let (result)
(dotimes (c 80 (reverse result))
(push (if (plusp c) (hsv (* 359 (/ c 80)) 1 1) 0) result))))
``````

Modify the definition of mandelbrot to:

``````(defun mandelbrot (x0 y0 scale)
(set-rotation 2)
(fill-screen)
(dotimes (y 240)
(let ((b (+ (/ (- y 120) 120 scale) y0)))
(dotimes (x 320)
(let* ((a (+ (/ (- x 160) 120 scale) x0))
(c (iterate a b)))
(draw-pixel x y (nth c *contours*)))))))
``````

The time taken to draw:

``````(mandelbrot -0.53 -0.61 11)
``````

is now just 26 seconds!