vydd / sketch

A Common Lisp framework for the creation of electronic art, visual design, game prototyping, game making, computer graphics, exploration of human-computer interaction, and more.
MIT License
1.39k stars 67 forks source link

Improve speed of canvas painting #112

Closed Kevinpgalligan closed 8 months ago

Kevinpgalligan commented 8 months ago

While trying to replicate Coding Train's Mandelbrot set sketch, I found that canvas painting is very slow.

Here's my code to create a canvas and draw a picture of the Mandelbrot set on it (requires alexandria for the utility function):

(defun make-mandelbrot-image (width height min-val max-val max-iters bound)
  (let ((canvas (make-canvas width height)))
    (dotimes (x width)
      (dotimes (y height)
        (let ((c (complex (remap x 0 width min-val max-val)
                          (remap y 0 height min-val max-val)))
              (z 0)
              (n 0))
          (loop while (and (< n max-iters) (< (abs z) bound))
                do (setf z (+ (* z z) c))
                do (incf n))
          (canvas-paint canvas
                        (gray-255 (remap n 0 max-iters 0 255))
                        x
                        y))))
    (canvas-lock canvas)
    canvas))

(defun remap (x la ha lb hb)
  "Takes x from the interval [la, ha] and remaps it to the
interval [lb, hb]. If x is outside the expected interval, then
it gets clamped."
  (setf x (alexandria:clamp x la ha))
  (+ lb (* (- hb lb) (/ (- x la) (- ha la)))))

Running (time (make-mandelbrot-image 100 100 -1 +1 10 16)), this takes over 9 seconds on my machine, even for this relatively small canvas size.

Extracting the logic to compute the value at each coordinate, it takes only 0.015 seconds. This indicates that the slowness comes from painting the canvas or generating the resulting image.

(time
   (let ((width 100) (height 100) (max-val 1) (min-val 1) (max-iters 10) (bound 16))
     (dotimes (x width)
       (dotimes (y height)
         (let ((c (complex (remap x 0 width min-val max-val)
                           (remap y 0 height min-val max-val)))
               (z 0)
               (n 0))
           (loop while (and (< n max-iters) (< (abs z) bound))
                 do (setf z (+ (* z z) c))
                 do (incf n)))))))

Finally, commenting out the (canvas-lock canvas) line in the original code, which (as far as I understand) is responsible for generating an image, it takes just as much time as before (9+ seconds). This leads me to believe that (canvas-paint) is the bottleneck. I am happy to dig into this and work on optimising it. Ideally, I think that pixel poking should be just as fast as in p5.js or Processing.

Gleefre commented 8 months ago

Hm, commenting out canvas-paint doesn't reduce time either.

Maybe the time difference comes from the fact that in the second snippet of code you have (min-val 1) while in the call to make-mandelbrot-image you pass -1?

Kevinpgalligan commented 8 months ago

Oh, woops, you are completely right! I guess that my crappy code is the problem. Was struggling to understand why it was taking much longer to render than in Coding Train's videos. Happy to close this, then.

vydd commented 8 months ago

@Kevinpgalligan remap exists in sketch as normalize. I really don't know why I decided to make the API use kwd args called "out-low" and "out-high". They probably should've been optionals.